Skip to content

Commit df280da

Browse files
committed
WIP: proptest_async
1 parent 3801c29 commit df280da

3 files changed

Lines changed: 108 additions & 83 deletions

File tree

Cargo.lock

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,6 @@ uuid = { version = "1.10.0", features = ["v7", "serde"] }
5454
proptest = { version = "1.9.0", optional = true }
5555
proptest-derive = { version = "0.7.0", optional = true }
5656
proptest-arbitrary-interop = { version = "0.1", optional = true }
57+
58+
[dev-dependencies]
59+
test-strategy = "0.4"

packages/backend/tests/user_state_tests.rs

Lines changed: 56 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,30 @@
22
mod tests {
33
use backend::app::AppError;
44
use backend::auth::PermissionLevel;
5-
use backend::user_state::UserState;
6-
use proptest::prelude::*;
5+
use backend::user_state::arbitrary::arbitrary_user_state_with_id;
6+
use backend::user_state::{UserState, read_user_state_from_db};
77
use sqlx::PgPool;
8-
9-
struct UserStateTestFixture {
10-
pool: PgPool,
8+
use test_strategy::proptest;
9+
10+
async fn get_pool() -> PgPool {
11+
let database_url =
12+
std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for tests");
13+
PgPool::connect(&database_url)
14+
.await
15+
.expect("Failed to connect to database")
1116
}
1217

13-
impl UserStateTestFixture {
14-
async fn setup() -> Self {
15-
let database_url =
16-
std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for tests");
17-
18-
let pool = PgPool::connect(&database_url).await.expect("Failed to connect to database");
19-
20-
Self { pool }
21-
}
22-
23-
async fn cleanup(&self, user_ids: &[&str]) {
24-
// TODO: find saner way to setup and cleanup a test db
25-
let _ = sqlx::query("DELETE FROM permissions WHERE subject = ANY($1)")
26-
.bind(user_ids)
27-
.execute(&self.pool)
28-
.await;
29-
30-
let _ = sqlx::query("DELETE FROM users WHERE id = ANY($1)")
31-
.bind(user_ids)
32-
.execute(&self.pool)
33-
.await;
34-
}
18+
async fn cleanup(pool: &PgPool, user_ids: &[&str]) {
19+
// TODO: find saner way to setup and cleanup a test db
20+
let _ = sqlx::query("DELETE FROM permissions WHERE subject = ANY($1)")
21+
.bind(user_ids)
22+
.execute(pool)
23+
.await;
24+
25+
let _ = sqlx::query("DELETE FROM users WHERE id = ANY($1)")
26+
.bind(user_ids)
27+
.execute(pool)
28+
.await;
3529
}
3630

3731
/// Writes user state to the database. This is only for testing purposes.
@@ -44,7 +38,7 @@ mod tests {
4438
/// Note: The owner of a document is determined by who has the 'own' permission.
4539
/// If the doc has an owner specified, that user gets 'own' permission.
4640
/// The requesting user gets their specified permission level.
47-
pub async fn write_user_state_to_db(
41+
async fn write_user_state_to_db(
4842
user_id: String,
4943
db: &PgPool,
5044
state: &UserState,
@@ -68,7 +62,7 @@ mod tests {
6862
user_id.clone()
6963
} else {
7064
// No owner specified and user doesn't own - skip this invalid case
71-
return user_id.clone();
65+
user_id.clone()
7266
}
7367
});
7468

@@ -147,61 +141,40 @@ mod tests {
147141
Ok(())
148142
}
149143

150-
proptest! {
151-
#[test]
152-
fn generates_user_states_always_true(_state in any::<UserState>()) {
153-
prop_assert!(true);
154-
}
155-
}
156-
157-
#[tokio::test]
158-
async fn user_state_roundtrip() {
159-
use backend::user_state::arbitrary::arbitrary_user_state_with_id;
160-
use backend::user_state::read_user_state_from_db;
161-
use proptest::strategy::ValueTree;
162-
use proptest::test_runner::TestRunner;
163-
164-
// Skip test if DATABASE_URL is not set
165-
if std::env::var("DATABASE_URL").is_err() {
166-
eprintln!("Skipping user_state_roundtrip: DATABASE_URL not set");
167-
return;
168-
}
169-
170-
let fixture = UserStateTestFixture::setup().await;
171-
172-
// Generate test cases synchronously
173-
let mut runner = TestRunner::default();
174-
let strategy = arbitrary_user_state_with_id();
175-
176-
for _ in 0..256 {
177-
let (user_id, state) =
178-
strategy.new_tree(&mut runner).expect("Failed to generate test case").current();
179-
180-
// Write state to db
181-
write_user_state_to_db(user_id.clone(), &fixture.pool, &state)
182-
.await
183-
.expect("Failed to write user state");
184-
185-
// Read state back from db
186-
let read_state = read_user_state_from_db(user_id.clone(), &fixture.pool)
187-
.await
188-
.expect("Failed to read user state");
189-
190-
// Cleanup test data
191-
let user_ids: Vec<&str> = std::iter::once(user_id.as_str())
192-
.chain(
193-
state.documents.iter().filter_map(|d| d.owner.as_ref().map(|o| o.id.as_str())),
194-
)
195-
.collect();
196-
fixture.cleanup(&user_ids).await;
144+
#[proptest(async = "tokio")]
145+
async fn user_state_roundtrip(
146+
#[strategy(arbitrary_user_state_with_id())] user_id_and_state: (String, UserState),
147+
) {
148+
let (user_id, state) = user_id_and_state;
149+
let pool = get_pool().await;
150+
151+
// Write state to db
152+
write_user_state_to_db(user_id.clone(), &pool, &state)
153+
.await
154+
.expect("Failed to write user state");
155+
156+
// Read state back from db
157+
let read_state = read_user_state_from_db(user_id.clone(), &pool)
158+
.await
159+
.expect("Failed to read user state");
160+
161+
// Cleanup test data
162+
let user_ids: Vec<&str> = std::iter::once(user_id.as_str())
163+
.chain(
164+
state
165+
.documents
166+
.iter()
167+
.filter_map(|d| d.owner.as_ref().map(|o| o.id.as_str())),
168+
)
169+
.collect();
170+
cleanup(&pool, &user_ids).await;
197171

198-
// Sort both document lists by ref_id for comparison (read returns sorted by created_at DESC)
199-
let mut expected_docs = state.documents.clone();
200-
let mut actual_docs = read_state.documents.clone();
201-
expected_docs.sort_by(|a, b| a.ref_id.cmp(&b.ref_id));
202-
actual_docs.sort_by(|a, b| a.ref_id.cmp(&b.ref_id));
172+
// Sort both document lists by ref_id for comparison (read returns sorted by created_at DESC)
173+
let mut expected_docs = state.documents.clone();
174+
let mut actual_docs = read_state.documents.clone();
175+
expected_docs.sort_by(|a, b| a.ref_id.cmp(&b.ref_id));
176+
actual_docs.sort_by(|a, b| a.ref_id.cmp(&b.ref_id));
203177

204-
assert_eq!(expected_docs, actual_docs, "UserState roundtrip failed");
205-
}
178+
proptest::prop_assert_eq!(expected_docs, actual_docs);
206179
}
207180
}

0 commit comments

Comments
 (0)