Skip to main content

max / makenotwork

Add a test-hygiene seal, hoist duplicated test seeding into the harness Thirteen byte-identical copies of seed_user across the db_*_layer workflows, and three of seed_project. Both now live in tests/harness/seed.rs, re-exported from the harness, with a note on when to prefer the harness methods that go through the real handlers instead. access_gate's copy was the one that differed: it took a can_create_projects flag and used a different email domain. It now calls seed_user then the existing grant_creator, and the reason for seeding rather than signing up (the signup flow is itself behind the gate under test) moved from the deleted function's doc comment to the call site, where it is load-bearing. tests/test_hygiene.rs is a ratchet in the same shape as migration_hygiene.rs and frontend_globals.rs. Two rules are hard, and already pass: every workflow module carries a //! header, every #[ignore] carries a reason. Three are HIGH_WATER counts too large to fix in one sweep, frozen at today's numbers so the pile can only drain: 830 loose status assertions, 176 test_-prefixed names, 11 modules over 800 lines. Each failure message names the number to set. Verified it bites by injecting violations and watching it fail. CONTRIBUTING.md gains the conventions the seal enforces and the seal inventory. tests/README.md had drifted badly: 986/679 test counts against a real 1,912/1,226, 7 of 11 harness constructors, and no mention of the seals. Verified: 1,912 lib + 1,226 integration + 5 seals green, clippy clean.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-04 21:09 UTC
Signed with PGP, not checked
Commit: 591a707eece44c96273845ab159b6b2cad4c2803
Parent: 50b4b58
18 files changed, +327 insertions, -243 deletions
@@ -454,6 +454,23 @@
454 454
455 455 On Astra, use `--test-threads=8` (or the `RUST_TEST_THREADS=8` env var) to avoid overwhelming PostgreSQL with concurrent database creation.
456 456
457 + ### Seal Tests
458 +
459 + `assumptions.rs`, `migration_hygiene.rs`, `frontend_globals.rs`, `test_hygiene.rs`, and `workflows/enum_drift.rs` are ratchets: they read the repo (the TOML corpus, the migration set, `static/*.js`, the test suite itself, the live schema) and fail when a convention is broken or a `HIGH_WATER` count rises. Add one whenever a rule is worth enforcing but too tedious to catch in review, and write the failure message so it names the number to set.
460 +
461 + `test_hygiene.rs` enforces the conventions below. Two of its rules are hard (every workflow module has a `//!` header; every `#[ignore]` carries a reason) and three are ratchets over counts that are too large to fix in one sweep: loose status assertions, `test_`-prefixed names, and modules over 800 lines. Lower a `HIGH_WATER` when you clean a file up; the failure message names the number.
462 +
463 + ### Conventions
464 +
465 + - **Name the behavior, not the function.** `lookups_return_none_when_absent`, not `test_lookup`. No `test_` prefix; the attribute already says it.
466 + - **Assert the exact status code.** `assert_eq!(resp.status, 403)` pins the contract. `is_client_error()` also passes on the 404 you get when the route silently disappears. Use the loose form only where the code genuinely is not contracted.
467 + - **Carry the body in the failure message**: `assert_eq!(resp.status, 200, "publish should succeed: {} {}", resp.status, resp.text)`.
468 + - **Shared setup lives in `tests/harness/`.** A per-file helper is fine when it is a thin wrapper over harness calls; the moment a second file wants it, promote it rather than copy it. One helper name means one return shape.
469 + - **Return named structs past two fields** (`CreatorSetup` is the model), not long tuples.
470 + - **Never sleep to wait for background work.** Drain it deterministically (`drain_scan_jobs`, `drain_s3_deletions`).
471 + - **`#[ignore]` needs a reason string** saying how to run the test instead.
472 + - Group integration tests by feature domain, one module per domain, each opening with a `//!` comment on what surface it covers. Split a module before it passes ~800 lines.
473 +
457 474 ## Rust Edition and Style
458 475
459 476 - **Rust 2024 edition** (Rust 1.85+). Uses `gen` keyword restrictions and other 2024 features.
@@ -9,11 +9,11 @@
9 9
10 10 ### Unit Tests (`cargo test --lib`)
11 11
12 - 986 tests covering pure logic: pricing, validation, formatting, enums, error handling, CSRF, RSS, file scanning, import parsing, etc. No database required.
12 + 1,912 tests covering pure logic: pricing, validation, formatting, enums, error handling, CSRF, RSS, file scanning, import parsing, etc. No database required. Runs in ~2.5s.
13 13
14 14 ### Integration Tests (`cargo test --test integration`)
15 15
16 - 679 tests across 78 workflow modules. Each test gets an isolated PostgreSQL database cloned from a shared template (migrations applied once per test run).
16 + 1,226 tests across 123 workflow modules. Each test gets an isolated PostgreSQL database cloned from a shared template (migrations applied once, then reused across runs while migration-current). ~433s at `--test-threads=8`.
17 17
18 18 **Harness features:**
19 19 - In-process Axum app (no network, uses `tower::ServiceExt::oneshot`)
@@ -31,6 +31,28 @@
31 31 - `with_admin()`: pre-created admin user
32 32 - `with_storage_and_scanner()`: file scanning pipeline
33 33 - `with_git_repos(path)`: git repository support
34 + - `with_admin_storage_and_scanner()`: admin user + in-memory S3 + scanner
35 + - `with_synckit_storage()`: SyncKit in-memory bucket (OTA tests)
36 + - `with_creator_tier_checkout()`: mock Stripe + a configured Everything-tier price
37 + - `with_postmark()`: Postmark webhook tokens configured
38 +
39 + Anything not covered by a constructor goes through `TestHarness::build(BuildOptions { .. })`
40 + directly; add a documented `BuildOptions` field rather than a constructor per calling test.
41 +
42 + **Seeding:** `harness/seed.rs` provides `seed_user` and `seed_project` for the
43 + `db_*_layer.rs` contract tests, which run against a bare `TestDb` with no router
44 + and so cannot use `signup`. Prefer the harness methods (`signup`, `create_creator`,
45 + `create_creator_with_item`) anywhere a session or HTTP flow is in play.
46 +
47 + ### Seal Tests
48 +
49 + Ratchets over the repo itself, each its own binary. No database.
50 +
51 + - `assumptions.rs` — the business assumptions TOML parses, validates, and still resolves every marker in the site-docs corpus.
52 + - `migration_hygiene.rs` — new migrations use `CONCURRENTLY` / `IF NOT EXISTS` and opt out of the per-migration transaction correctly.
53 + - `frontend_globals.rs` — the `window.*` global count only goes down.
54 + - `test_hygiene.rs` — test-suite conventions: doc headers, `#[ignore]` reasons, and `HIGH_WATER` counts for loose status assertions, `test_` prefixes, and oversized modules.
55 + - `workflows/enum_drift.rs` — every domain enum's variants match its Postgres `CHECK` list (needs a DB; runs inside the integration binary).
34 56
35 57 ### Load Tests (`cargo test --test load -- --ignored --nocapture`)
36 58
@@ -4,9 +4,13 @@
4 4 pub(crate) mod db;
5 5 pub(crate) mod email;
6 6 pub(crate) mod gitfixture;
7 + pub(crate) mod seed;
7 8 pub(crate) mod storage;
8 9 pub(crate) mod stripe;
9 10
11 + #[allow(unused_imports)]
12 + pub(crate) use seed::{seed_project, seed_user};
13 +
10 14 /// CDN render base every test app uses unless `Opts::cdn_base_url` overrides it.
11 15 /// `Config::cdn_base_url` is required, so tests always have one.
12 16 pub(crate) const TEST_CDN_BASE: &str = "https://cdn.test";
@@ -1,9 +1,9 @@
1 1 //! Site access gate (`ACCESS_GATE=fan_plus_or_creator`): the testnot-style
2 2 //! gate that restricts the whole site to creators and Fan+ members.
3 3
4 + use crate::harness::seed_user;
4 5 use crate::harness::{BuildOptions, TestHarness};
5 6 use makenotwork::config::AccessGate;
6 - use sqlx::PgPool;
7 7
8 8 fn location(resp: &crate::harness::client::TestResponse) -> String {
9 9 resp.headers
@@ -13,23 +13,6 @@
13 13 .to_string()
14 14 }
15 15
16 - /// Insert a verified user directly (the HTTP signup flow is itself gated on
17 - /// testnot, so the test seeds the DB and authenticates via the exempt /login).
18 - async fn seed_user(pool: &PgPool, username: &str, can_create_projects: bool) {
19 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
20 - sqlx::query(
21 - "INSERT INTO users (username, email, password_hash, email_verified, can_create_projects)
22 - VALUES ($1, $2, $3, true, $4)",
23 - )
24 - .bind(username)
25 - .bind(format!("{username}@example.com"))
26 - .bind(&hash)
27 - .bind(can_create_projects)
28 - .execute(pool)
29 - .await
30 - .expect("seed user");
31 - }
32 -
33 16 #[tokio::test]
34 17 async fn access_gate_restricts_to_fan_plus_or_creator() {
35 18 let mut h = TestHarness::build(BuildOptions {
@@ -37,8 +20,12 @@
37 20 ..Default::default()
38 21 })
39 22 .await;
40 - seed_user(&h.db, "gatecreator", true).await;
41 - seed_user(&h.db, "plainfan", false).await;
23 + // Seeded directly rather than via `signup`: the HTTP signup flow is itself
24 + // behind this gate, so the test seeds the rows and authenticates through the
25 + // exempt `/login`.
26 + let creator = seed_user(&h.db, "gatecreator").await;
27 + h.grant_creator(creator).await;
28 + seed_user(&h.db, "plainfan").await;
42 29
43 30 // Anonymous visitor → bounced to login with the gate notice.
44 31 let r = h.client.get("/").await;
@@ -14,32 +14,8 @@
14 14 //! invariant, every item is present exactly once, not position distinctness.
15 15
16 16 use crate::harness::db::TestDb;
17 - use makenotwork::db::{ItemId, ProjectId, Slug, UserId, collections};
18 -
19 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
20 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
21 - sqlx::query_scalar::<_, UserId>(
22 - "INSERT INTO users (username, email, password_hash, email_verified)
23 - VALUES ($1, $2, $3, true) RETURNING id",
24 - )
25 - .bind(username)
26 - .bind(format!("{username}@test.com"))
27 - .bind(&hash)
28 - .fetch_one(pool)
29 - .await
30 - .expect("seed user")
31 - }
32 -
33 - async fn seed_project(pool: &sqlx::PgPool, user: UserId, slug: &str) -> ProjectId {
34 - sqlx::query_scalar::<_, ProjectId>(
35 - "INSERT INTO projects (user_id, slug, title) VALUES ($1, $2, 'P') RETURNING id",
36 - )
37 - .bind(user)
38 - .bind(slug)
39 - .fetch_one(pool)
40 - .await
41 - .expect("seed project")
42 - }
17 + use crate::harness::{seed_project, seed_user};
18 + use makenotwork::db::{ItemId, ProjectId, Slug, collections};
43 19
44 20 async fn seed_item(pool: &sqlx::PgPool, project: ProjectId, slug: &str) -> ItemId {
45 21 sqlx::query_scalar::<_, ItemId>(
@@ -16,25 +16,11 @@
16 16 //! resubscribe) and the 30-day grace window resolve as documented.
17 17
18 18 use crate::harness::db::TestDb;
19 + use crate::harness::seed_user;
19 20 use makenotwork::db::{self, CreatorTier, UserId};
20 21
21 22 const MB: i64 = 1024 * 1024;
22 23
23 - /// Seed a minimal verified user (rows FK `users(id)`).
24 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
25 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
26 - sqlx::query_scalar::<_, UserId>(
27 - "INSERT INTO users (username, email, password_hash, email_verified)
28 - VALUES ($1, $2, $3, true) RETURNING id",
29 - )
30 - .bind(username)
31 - .bind(format!("{username}@test.com"))
32 - .bind(&hash)
33 - .fetch_one(pool)
34 - .await
35 - .expect("seed user")
36 - }
37 -
38 24 async fn storage_used(pool: &sqlx::PgPool, user: UserId) -> i64 {
39 25 db::creator_tiers::get_storage_used(pool, user)
40 26 .await
@@ -12,22 +12,9 @@
12 12 //! it. The concurrency test stays within that guaranteed envelope.
13 13
14 14 use crate::harness::db::TestDb;
15 + use crate::harness::seed_user;
15 16 use makenotwork::db::{GitRepoId, UserId, issues};
16 17
17 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
18 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
19 - sqlx::query_scalar::<_, UserId>(
20 - "INSERT INTO users (username, email, password_hash, email_verified)
21 - VALUES ($1, $2, $3, true) RETURNING id",
22 - )
23 - .bind(username)
24 - .bind(format!("{username}@test.com"))
25 - .bind(&hash)
26 - .fetch_one(pool)
27 - .await
28 - .expect("seed user")
29 - }
30 -
31 18 async fn seed_repo(pool: &sqlx::PgPool, user: UserId, name: &str) -> GitRepoId {
32 19 sqlx::query_scalar::<_, GitRepoId>(
33 20 "INSERT INTO git_repos (user_id, name) VALUES ($1, $2) RETURNING id",
@@ -10,33 +10,9 @@
10 10 //! slugs and both succeed.
11 11
12 12 use crate::harness::db::TestDb;
13 + use crate::harness::{seed_project, seed_user};
13 14 use makenotwork::db::items;
14 - use makenotwork::db::{ItemId, PriceCents, ProjectId, TagId, UserId};
15 -
16 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
17 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
18 - sqlx::query_scalar::<_, UserId>(
19 - "INSERT INTO users (username, email, password_hash, email_verified)
20 - VALUES ($1, $2, $3, true) RETURNING id",
21 - )
22 - .bind(username)
23 - .bind(format!("{username}@test.com"))
24 - .bind(&hash)
25 - .fetch_one(pool)
26 - .await
27 - .expect("seed user")
28 - }
29 -
30 - async fn seed_project(pool: &sqlx::PgPool, user: UserId, slug: &str) -> ProjectId {
31 - sqlx::query_scalar::<_, ProjectId>(
32 - "INSERT INTO projects (user_id, slug, title) VALUES ($1, $2, 'P') RETURNING id",
33 - )
34 - .bind(user)
35 - .bind(slug)
36 - .fetch_one(pool)
37 - .await
38 - .expect("seed project")
39 - }
15 + use makenotwork::db::{ItemId, PriceCents, ProjectId, TagId};
40 16
41 17 /// Seed an item with an explicit sort_order and a unique slug.
42 18 async fn seed_item(pool: &sqlx::PgPool, project: ProjectId, slug: &str, sort: i32) -> ItemId {
@@ -9,6 +9,7 @@
9 9 //! post-grace hide/unhide round-trip, and the admin-removal republish block.
10 10
11 11 use crate::harness::TestHarness;
12 + use crate::harness::{seed_project, seed_user};
12 13 use makenotwork::db::{AiTier, ItemId, ItemType, PriceCents, ProjectId, UserId, items};
13 14
14 15 /// Create a logged-in creator with an empty-ish project and return
@@ -22,34 +23,6 @@
22 23 (setup.user_id, project_id)
23 24 }
24 25
25 - /// Seed a bare user directly (no HTTP-created item), for tests that assert on a
26 - /// per-user, project-spanning count and so need a contamination-free item set.
27 - async fn seed_user(h: &TestHarness, username: &str) -> UserId {
28 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
29 - sqlx::query_scalar::<_, UserId>(
30 - "INSERT INTO users (username, email, password_hash, email_verified)
31 - VALUES ($1, $2, $3, true) RETURNING id",
32 - )
33 - .bind(username)
34 - .bind(format!("{username}@test.com"))
35 - .bind(&hash)
36 - .fetch_one(&h.db)
37 - .await
38 - .expect("seed user")
39 - }
40 -
41 - /// Seed a bare project directly for a user.
42 - async fn seed_project(h: &TestHarness, user: UserId, slug: &str) -> ProjectId {
43 - sqlx::query_scalar::<_, ProjectId>(
44 - "INSERT INTO projects (user_id, slug, title) VALUES ($1, $2, 'P') RETURNING id",
45 - )
46 - .bind(user)
47 - .bind(slug)
48 - .fetch_one(&h.db)
49 - .await
50 - .expect("seed project")
51 - }
52 -
53 26 /// Insert a fresh item into a project via the real `create_item` path.
54 27 async fn make_item(h: &TestHarness, project: ProjectId, title: &str) -> ItemId {
55 28 let item = items::create_item(
@@ -269,8 +242,8 @@
269 242 let h = TestHarness::new().await;
270 243 // Seed a contamination-free creator: `hide_all_items_for_users` spans every
271 244 // project the user owns, so the count must not include a stray HTTP-seeded item.
272 - let owner = seed_user(&h, "itm_hide_owner").await;
273 - let project = seed_project(&h, owner, "itm-hide-proj").await;
245 + let owner = seed_user(&h.db, "itm_hide_owner").await;
246 + let project = seed_project(&h.db, owner, "itm-hide-proj").await;
274 247 // create_item leaves is_public at its column default (true), so both start public.
275 248 let a = make_item(&h, project, "Public A").await;
276 249 let b = make_item(&h, project, "Public B").await;
@@ -8,23 +8,10 @@
8 8 //! owner, and the post-auth update bumps the stored counter.
9 9
10 10 use crate::harness::db::TestDb;
11 - use makenotwork::db::{UserId, passkeys};
11 + use crate::harness::seed_user;
12 + use makenotwork::db::passkeys;
12 13 use serde_json::json;
13 14
14 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
15 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
16 - sqlx::query_scalar::<_, UserId>(
17 - "INSERT INTO users (username, email, password_hash, email_verified)
18 - VALUES ($1, $2, $3, true) RETURNING id",
19 - )
20 - .bind(username)
21 - .bind(format!("{username}@test.com"))
22 - .bind(&hash)
23 - .fetch_one(pool)
24 - .await
25 - .expect("seed user")
26 - }
27 -
28 15 #[tokio::test]
29 16 async fn create_then_find_by_credential_id_roundtrips_the_owner() {
30 17 let db = TestDb::new().await;
@@ -10,25 +10,12 @@
10 10 //! resurrecting a terminal row.
11 11
12 12 use crate::harness::TestHarness;
13 + use crate::harness::seed_user;
13 14 use makenotwork::db::UserId;
14 15 use makenotwork::db::scan_jobs::{self, ScanTargetKind};
15 16 use makenotwork::storage::FileType;
16 17 use uuid::Uuid;
17 18
18 - async fn seed_user(h: &TestHarness, name: &str) -> UserId {
19 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
20 - sqlx::query_scalar::<_, UserId>(
21 - "INSERT INTO users (username, email, password_hash, email_verified)
22 - VALUES ($1, $2, $3, true) RETURNING id",
23 - )
24 - .bind(name)
25 - .bind(format!("{name}@test.com"))
26 - .bind(&hash)
27 - .fetch_one(&h.db)
28 - .await
29 - .expect("seed user")
30 - }
31 -
32 19 async fn status_of(h: &TestHarness, job: Uuid) -> String {
33 20 sqlx::query_scalar::<_, String>("SELECT status FROM scan_jobs WHERE id = $1")
34 21 .bind(job)
@@ -63,7 +50,7 @@
63 50 #[tokio::test]
64 51 async fn reaper_spares_slow_but_alive_job() {
65 52 let h = TestHarness::new().await;
66 - let user = seed_user(&h, "reaper_alive").await;
53 + let user = seed_user(&h.db, "reaper_alive").await;
67 54 let job = enqueue_and_claim(&h, user, "k/alive").await;
68 55
69 56 // A job running far past the stuck window but whose worker is still beating:
@@ -85,7 +72,7 @@
85 72 #[tokio::test]
86 73 async fn reaper_reaps_crashed_worker() {
87 74 let h = TestHarness::new().await;
88 - let user = seed_user(&h, "reaper_dead").await;
75 + let user = seed_user(&h.db, "reaper_dead").await;
89 76 let job = enqueue_and_claim(&h, user, "k/dead").await;
90 77
91 78 // Worker crashed: no more beats.
@@ -107,7 +94,7 @@
107 94 // Any row with heartbeat_at NULL (e.g. one in flight across the migration)
108 95 // stays reapable on the old started_at clock via COALESCE.
109 96 let h = TestHarness::new().await;
110 - let user = seed_user(&h, "reaper_null").await;
97 + let user = seed_user(&h.db, "reaper_null").await;
111 98 let job = enqueue_and_claim(&h, user, "k/null").await;
112 99
113 100 sqlx::query(
@@ -125,7 +112,7 @@
125 112 #[tokio::test]
126 113 async fn bump_heartbeat_refreshes_and_never_resurrects() {
127 114 let h = TestHarness::new().await;
128 - let user = seed_user(&h, "reaper_bump").await;
115 + let user = seed_user(&h.db, "reaper_bump").await;
129 116 let job = enqueue_and_claim(&h, user, "k/bump").await;
130 117
131 118 // A stale beat would be reaped...
@@ -7,21 +7,8 @@
7 7 //! unambiguous, one account can't register another's key fingerprint.
8 8
9 9 use crate::harness::db::TestDb;
10 - use makenotwork::db::{UserId, ssh_keys};
11 -
12 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
13 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
14 - sqlx::query_scalar::<_, UserId>(
15 - "INSERT INTO users (username, email, password_hash, email_verified)
16 - VALUES ($1, $2, $3, true) RETURNING id",
17 - )
18 - .bind(username)
19 - .bind(format!("{username}@test.com"))
20 - .bind(&hash)
21 - .fetch_one(pool)
22 - .await
23 - .expect("seed user")
24 - }
10 + use crate::harness::seed_user;
11 + use makenotwork::db::ssh_keys;
25 12
26 13 #[tokio::test]
27 14 async fn add_then_lookup_resolves_the_owner() {
@@ -17,25 +17,11 @@
17 17 //! - `claim_key` / `release_key` are idempotent.
18 18
19 19 use crate::harness::db::TestDb;
20 + use crate::harness::seed_user;
20 21 use chrono::{DateTime, Utc};
21 22 use makenotwork::db::synckit_billing;
22 23 use makenotwork::db::{SyncAppId, SyncBillingStatus, SyncEnforcementMode, UserId};
23 24
24 - /// Seed a verified user.
25 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
26 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
27 - sqlx::query_scalar::<_, UserId>(
28 - "INSERT INTO users (username, email, password_hash, email_verified)
29 - VALUES ($1, $2, $3, true) RETURNING id",
30 - )
31 - .bind(username)
32 - .bind(format!("{username}@test.com"))
33 - .bind(&hash)
34 - .fetch_one(pool)
35 - .await
36 - .expect("seed user")
37 - }
38 -
39 25 /// Seed a non-internal draft sync app plus its live-usage row (the row the
40 26 /// storage layer normally inserts on app create, and that `claim_key` locks
41 27 /// `FOR UPDATE`).
@@ -8,25 +8,11 @@
8 8 //! personal query. Design: wiki synckit-groups-design.
9 9
10 10 use crate::harness::db::TestDb;
11 + use crate::harness::seed_user;
11 12 use makenotwork::db::synckit;
12 13 use makenotwork::db::{SyncAppId, SyncDeviceId, SyncGroupId, UserId};
13 14 use uuid::Uuid;
14 15
15 - /// Seed a verified user.
16 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
17 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
18 - sqlx::query_scalar::<_, UserId>(
19 - "INSERT INTO users (username, email, password_hash, email_verified)
20 - VALUES ($1, $2, $3, true) RETURNING id",
21 - )
22 - .bind(username)
23 - .bind(format!("{username}@test.com"))
24 - .bind(&hash)
25 - .fetch_one(pool)
26 - .await
27 - .expect("seed user")
28 - }
29 -
30 16 /// Seed a sync app owned by `user`.
31 17 async fn seed_app(pool: &sqlx::PgPool, user: UserId, name: &str) -> SyncAppId {
32 18 sqlx::query_scalar::<_, SyncAppId>(
@@ -10,24 +10,10 @@
10 10 //! directly against real Postgres.
11 11
12 12 use crate::harness::db::TestDb;
13 + use crate::harness::seed_user;
13 14 use makenotwork::db::synckit;
14 15 use makenotwork::db::{SyncAppId, SyncDeviceId, UserId};
15 16
16 - /// Seed a verified user.
17 - async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId {
18 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
19 - sqlx::query_scalar::<_, UserId>(
20 - "INSERT INTO users (username, email, password_hash, email_verified)
21 - VALUES ($1, $2, $3, true) RETURNING id",
22 - )
23 - .bind(username)
24 - .bind(format!("{username}@test.com"))
25 - .bind(&hash)
26 - .fetch_one(pool)
27 - .await
28 - .expect("seed user")
29 - }
30 -
31 17 /// Seed a sync app owned by `user`.
32 18 async fn seed_app(pool: &sqlx::PgPool, user: UserId, name: &str) -> SyncAppId {
33 19 sqlx::query_scalar::<_, SyncAppId>(
@@ -5,23 +5,8 @@
5 5 //! first-writer-wins under ON CONFLICT, and the 24-hour cleanup boundary.
6 6
7 7 use crate::harness::db::TestDb;
8 - use makenotwork::db::{UserId, idempotency};
9 - use sqlx::PgPool;
10 -
11 - /// Seed a minimal verified user and return its id (the cache FKs `users(id)`).
12 - async fn seed_user(pool: &PgPool, username: &str) -> UserId {
13 - let hash = makenotwork::auth::hash_password("password123").expect("hash");
14 - sqlx::query_scalar::<_, UserId>(
15 - "INSERT INTO users (username, email, password_hash, email_verified)
16 - VALUES ($1, $2, $3, true) RETURNING id",
17 - )
18 - .bind(username)
19 - .bind(format!("{username}@test.com"))
20 - .bind(&hash)
21 - .fetch_one(pool)
22 - .await
23 - .expect("seed user")
24 - }
8 + use crate::harness::seed_user;
9 + use makenotwork::db::idempotency;
25 10
26 11 #[tokio::test]
27 12 async fn store_then_get_roundtrip() {