max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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() { |