//! DB-layer contract tests for the `--seed-examples` flow (`seed::run`, Phases 1-2). //! //! These exercise the seed against real Postgres to prove the invariants the //! example marketplace rests on: //! //! 1. It clears the no-real-users guard on a *freshly-migrated* DB (migration //! `080_remove_demo_data` leaves no non-example accounts) and creates the //! full roster. //! 2. Every seeded creator+project is publicly visible, `is_sandbox = FALSE` //! on the user and `is_public = true` on the project, the only two gates the //! discover feed applies. The discover predicate returning them is the proof. //! 3. Phase-2 content: one item of every `ItemType`, spanning every pricing //! kind, with tags and subscription tiers, and all items **hidden** //! (`scan_status='pending'`) until Phase 3 attaches media. //! 4. Idempotency: a second `run()` wipes the prior example data first, so //! counts stay fixed instead of doubling. //! 5. The prod-host guard still refuses even with the opt-in flag set. use std::sync::Arc; use crate::harness::db::TestDb; use crate::harness::storage::InMemoryStorage; use makenotwork::seed::{self, SeedMedia, SeedOptions}; use makenotwork::storage::StorageBackend; /// The Phase-1 roster size (see `seed::creators::ROSTER`). pub(super) const SEEDED_CREATORS: i64 = 5; /// Phase-2 item count: five per project, spanning every `ItemType`. Wide enough /// that the storefront grids read as grids. const SEEDED_ITEMS: i64 = 25; /// Every `ItemType` must still appear at least once across the catalog. Held /// separately from the item count now that projects carry several of a type. const ITEM_TYPES: i64 = 11; /// Guard-passing options for a test run: opt-in on, approved example host. pub(super) fn testnot_opts() -> SeedOptions { SeedOptions { allow_example_seed: true, host_url: "https://testnot.work".to_string(), harness: None, buyer: None, } } /// Seeded creators and harness accounts, which is to say every example account /// except the background-buyer pool. Those are seeded by `seed::sales` purely to /// own transactions and are counted by /// [`every_item_reports_the_sales_it_actually_has`] instead; folding them in here /// would make the roster size a function of how many sales the demo shows. pub(super) async fn count_example_creators(pool: &sqlx::PgPool) -> i64 { sqlx::query_scalar( "SELECT COUNT(*) FROM users \ WHERE lower(split_part(email, '@', 2)) = 'example.test' AND is_sandbox = FALSE \ AND username NOT LIKE $1", ) .bind(format!("{}%", seed::sales::BUYER_HANDLE_PREFIX)) .fetch_one(pool) .await .expect("count example creators") } async fn count_public_example_projects(pool: &sqlx::PgPool) -> i64 { sqlx::query_scalar( "SELECT COUNT(*) FROM projects p \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email, '@', 2)) = 'example.test' AND p.is_public = TRUE", ) .fetch_one(pool) .await .expect("count public example projects") } /// Slugs of seeded projects that clear the *exact* public-discover gate /// (`p.is_public AND u.is_sandbox = FALSE`, per `db::discover::discover_projects`). /// Mirrors that predicate in SQL because the `discover` module is `pub(crate)` and /// so unreachable from this external test crate. pub(super) async fn discover_visible_example_slugs(pool: &sqlx::PgPool) -> Vec { sqlx::query_scalar::<_, String>( "SELECT p.slug::text FROM projects p \ JOIN users u ON u.id = p.user_id \ WHERE p.is_public = TRUE AND u.is_sandbox = FALSE \ AND lower(split_part(u.email, '@', 2)) = 'example.test' \ ORDER BY p.slug", ) .fetch_all(pool) .await .expect("discover-visible example slugs") } /// Total items under the seeded example projects. pub(super) async fn count_example_items(pool: &sqlx::PgPool) -> i64 { sqlx::query_scalar( "SELECT COUNT(*) FROM items i \ JOIN projects p ON p.id = i.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email, '@', 2)) = 'example.test'", ) .fetch_one(pool) .await .expect("count example items") } /// Example items that clear the *exact* item-discover gate. Phase 2 seeds items /// hidden (`scan_status='pending'`), so this must be zero until Phase 3. async fn count_discover_visible_example_items(pool: &sqlx::PgPool) -> i64 { sqlx::query_scalar( "SELECT COUNT(*) FROM items i \ JOIN projects p ON p.id = i.project_id \ JOIN users u ON u.id = p.user_id \ WHERE i.is_public = TRUE AND i.listed = TRUE AND p.is_public = TRUE \ AND i.scan_status = 'clean' AND u.is_sandbox = FALSE AND i.deleted_at IS NULL \ AND lower(split_part(u.email, '@', 2)) = 'example.test'", ) .fetch_one(pool) .await .expect("count discover-visible example items") } #[tokio::test] async fn seeds_publicly_visible_creators_and_projects() { let db = TestDb::new().await; // Precondition the guard relies on: a freshly-migrated DB holds no real // accounts (080_remove_demo_data cleared the 003 demo seed). let real: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM users WHERE lower(email) NOT LIKE '%@example.test'", ) .fetch_one(&db.pool) .await .expect("count real users"); assert_eq!(real, 0, "a migrated DB should hold no non-example accounts"); seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("seed should run on a clean migrated DB"); // All five creators exist and are non-sandbox (publicly visible), and each // owns a public project. assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS); assert_eq!( count_public_example_projects(&db.pool).await, SEEDED_CREATORS ); // Public visibility proof: all five projects clear the discover gate. let slugs = discover_visible_example_slugs(&db.pool).await; assert_eq!(slugs.len() as i64, SEEDED_CREATORS); for slug in [ "restored-reels-vol-1", "deskriver-suite", "cc0-field-library", "the-marginalia-reader", "commons-sampler", ] { assert!( slugs.iter().any(|s| s == slug), "discover-visible set missing seeded project {slug}" ); } } #[tokio::test] async fn seeds_items_across_types_and_pricing_but_hidden() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("seed"); // Five items per project, and every ItemType still represented. assert_eq!(count_example_items(&db.pool).await, SEEDED_ITEMS); let distinct_types: i64 = sqlx::query_scalar( "SELECT COUNT(DISTINCT i.item_type) FROM items i \ JOIN projects p ON p.id = i.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email, '@', 2)) = 'example.test'", ) .fetch_one(&db.pool) .await .expect("distinct item types"); assert_eq!(distinct_types, ITEM_TYPES, "every ItemType should appear"); // Phase-2 boundary: nothing is discover-visible yet (all pending). assert_eq!( count_discover_visible_example_items(&db.pool).await, 0, "Phase 2 items must stay hidden until Phase 3 attaches media" ); // Pricing kinds are represented: Pwyw (pwyw_enabled), BuyOnce (price > 0), // and Free (price 0) items all exist. let pwyw: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM items i JOIN projects p ON p.id = i.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' AND i.pwyw_enabled = TRUE", ) .fetch_one(&db.pool) .await .expect("pwyw count"); assert!(pwyw >= 1, "expected Pwyw items"); let buyonce: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM items i JOIN projects p ON p.id = i.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' \ AND i.pwyw_enabled = FALSE AND i.price_cents > 0", ) .fetch_one(&db.pool) .await .expect("buyonce count"); assert!(buyonce >= 1, "expected BuyOnce items"); // The subscription project has its pricing_model set and >= 2 tiers. let sub_tiers: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM subscription_tiers st JOIN projects p ON p.id = st.project_id \ WHERE p.slug = 'the-marginalia-reader' AND p.pricing_model = 'subscription'", ) .fetch_one(&db.pool) .await .expect("subscription tiers"); assert!( sub_tiers >= 2, "subscription project should have >= 2 tiers, got {sub_tiers}" ); // Every item is tagged (for the browse/filter surfaces), no example item // lacks a tag, which would mean a roster slug failed to resolve. let untagged: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM items i \ JOIN projects p ON p.id = i.project_id JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' \ AND NOT EXISTS (SELECT 1 FROM item_tags it WHERE it.item_id = i.id)", ) .fetch_one(&db.pool) .await .expect("untagged item count"); assert_eq!( untagged, 0, "every seeded item should have at least one tag" ); } /// Only the subscription project may be non-free at the project level. /// /// A paid project renders `ProjectPaywallTemplate` to anyone without access, and /// that template lists no items at all. Every testnot visitor is anonymous and /// therefore permanently without access, so a paid project is a storefront nobody /// can see inside. Spreading the four `PricingKind`s across projects hides most /// of the seeded catalog, which this guards against. Paid *items* are fine and /// are how BuyOnce and Pwyw get demonstrated. #[tokio::test] async fn only_the_subscription_project_is_paywalled() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("seed should succeed"); let paywalled: Vec = sqlx::query_scalar( "SELECT p.slug FROM projects p JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' \ AND p.pricing_model IS NOT NULL AND p.pricing_model <> 'free' \ ORDER BY p.slug", ) .fetch_all(&db.pool) .await .expect("paywalled project slugs"); assert_eq!( paywalled, vec!["the-marginalia-reader".to_string()], "only the subscription project may be paywalled; the rest must be \ project-level free so their items are reachable anonymously" ); } #[tokio::test] async fn every_item_reports_the_sales_it_actually_has() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("seed should succeed"); // The exact query `scheduler::integrity::check_sales_count_drift` runs. It // pages WAM on every row it returns, so a demo whose sales figures were // written rather than earned would alert forever. let drifted: Vec<(String, i32, i64)> = sqlx::query_as( r" SELECT i.title, i.sales_count, COUNT(t.id) FROM items i LEFT JOIN transactions t ON t.item_id = i.id AND t.status = 'completed' GROUP BY i.id, i.title, i.sales_count HAVING i.sales_count != COUNT(t.id) ", ) .fetch_all(&db.pool) .await .expect("drift query"); assert!( drifted.is_empty(), "sales_count disagrees with the transactions behind it: {drifted:?}" ); // And the figure is not uniformly zero, which is the state this phase // exists to leave behind. let sold: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM items WHERE sales_count > 0") .fetch_one(&db.pool) .await .expect("count of items with sales"); assert!(sold > 0, "no item reports a single sale"); // The pool those transactions belong to. Counted here because // `count_example_creators` deliberately excludes it. let buyers: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE username LIKE $1") .bind(format!("{}%", seed::sales::BUYER_HANDLE_PREFIX)) .fetch_one(&db.pool) .await .expect("count background buyers"); assert_eq!(buyers, seed::sales::BUYER_POOL as i64); } #[tokio::test] async fn no_item_is_released_on_the_day_the_seed_ran() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("seed should succeed"); // `items.created_at` is what the item page renders as "Released". A catalog // that all came out today is the clearest tell that it was generated. let stamped: Vec = sqlx::query_scalar( "SELECT title FROM items WHERE created_at > NOW() - INTERVAL '1 day' ORDER BY title", ) .fetch_all(&db.pool) .await .expect("release dates"); assert!( stamped.is_empty(), "these items carry the seed run's own date as their release: {stamped:?}" ); } #[tokio::test] async fn seed_is_idempotent() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("first seed"); seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("second seed"); // Re-running wipes prior example data first (cascading to projects + items), // so counts stay fixed instead of doubling. assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS); assert_eq!( count_public_example_projects(&db.pool).await, SEEDED_CREATORS ); assert_eq!(count_example_items(&db.pool).await, SEEDED_ITEMS); } #[tokio::test] async fn refuses_on_prod_host() { let db = TestDb::new().await; let err = seed::run( &db.pool, &SeedOptions { allow_example_seed: true, host_url: "https://makenot.work".to_string(), harness: None, buyer: None, }, &SeedMedia::none(), ) .await .expect_err("prod host must be refused"); assert!(matches!(err, seed::SeedError::ProdHost(_))); // Nothing was created, and the demo account is untouched. assert_eq!(count_example_creators(&db.pool).await, 0); } #[tokio::test] async fn seeds_blog_posts_and_follows() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("seed"); // Two published posts per project (10), all published. let posts: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM blog_posts bp JOIN projects p ON p.id = bp.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' AND bp.published_at IS NOT NULL", ) .fetch_one(&db.pool) .await .expect("published posts"); assert_eq!(posts, 10, "expected 2 published blog posts per project"); // Follow graph: 20 user-follows + 20 project-follows among example accounts. let user_follows: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM follows f JOIN users fu ON fu.id = f.follower_id \ WHERE f.target_type = 'user' AND lower(split_part(fu.email,'@',2))='example.test'", ) .fetch_one(&db.pool) .await .expect("user follows"); assert_eq!(user_follows, 20); let project_follows: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM follows f JOIN users fu ON fu.id = f.follower_id \ WHERE f.target_type = 'project' AND lower(split_part(fu.email,'@',2))='example.test'", ) .fetch_one(&db.pool) .await .expect("project follows"); assert_eq!(project_follows, 20); // Spot-check a rendered count: each creator has 4 followers. let followers_of_openreels: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM follows f \ WHERE f.target_type = 'user' \ AND f.target_id = (SELECT id FROM users WHERE username = 'openreels')", ) .fetch_one(&db.pool) .await .expect("openreels followers"); assert_eq!(followers_of_openreels, 4); // Idempotent: re-run keeps 10 posts + 40 follows, not doubled. seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("re-seed"); let posts2: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM blog_posts bp JOIN projects p ON p.id = bp.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test'", ) .fetch_one(&db.pool) .await .expect("posts after re-run"); assert_eq!(posts2, 10, "re-run should not duplicate blog posts"); let follows2: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM follows f JOIN users fu ON fu.id = f.follower_id \ WHERE lower(split_part(fu.email,'@',2))='example.test'", ) .fetch_one(&db.pool) .await .expect("follows after re-run"); assert_eq!(follows2, 40, "re-run should not duplicate follows"); } /// Guard-passing options bundled with an in-memory storage backend, so the media /// phase runs and flips items visible. pub(super) fn media_ctx() -> SeedMedia { let s3: Arc = Arc::new(InMemoryStorage::new()); let public: Arc = Arc::new(InMemoryStorage::new()); SeedMedia { s3: Some(s3), public_s3: Some(public), cdn_base_url: Some("https://cdn.example.test".to_string()), // No curated assets: every slot takes its generated placeholder, and the // test never reaches the network. Resolving the real manifest is a // separate, network-touching concern (`seed::manifest`). assets: makenotwork::seed::manifest::ResolvedAssets::default(), } } #[tokio::test] async fn seeds_media_makes_items_visible() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &media_ctx()) .await .expect("seed with media"); // With media attached, every item is promoted 'clean' and now clears the // full item-discover gate (contrast the no-storage `..._but_hidden` test). assert_eq!( count_discover_visible_example_items(&db.pool).await, SEEDED_ITEMS, "all items should be visible once media is attached" ); // The audio item has its audio key set. let audio_keyed: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM items i JOIN projects p ON p.id = i.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' \ AND i.item_type = 'audio' AND i.audio_s3_key IS NOT NULL", ) .fetch_one(&db.pool) .await .expect("audio keyed"); assert!(audio_keyed >= 1, "audio item should have an audio_s3_key"); // Download-type items produced clean, keyed versions. let clean_versions: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM versions v JOIN items i ON i.id = v.item_id \ JOIN projects p ON p.id = i.project_id JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' \ AND v.s3_key IS NOT NULL AND v.scan_status = 'clean'", ) .fetch_one(&db.pool) .await .expect("clean versions"); assert!( clean_versions >= 1, "expected a clean, keyed download version" ); // Covers were attached (public bucket + CDN base present). let covered: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM items i JOIN projects p ON p.id = i.project_id \ JOIN users u ON u.id = p.user_id \ WHERE lower(split_part(u.email,'@',2))='example.test' \ AND i.cover_s3_key IS NOT NULL AND i.cover_scan_status = 'clean'", ) .fetch_one(&db.pool) .await .expect("covered items"); assert!( covered >= 1, "expected item covers with cover_scan_status clean" ); // Idempotent with media too: re-running stays at the roster size. seed::run(&db.pool, &testnot_opts(), &media_ctx()) .await .expect("re-seed with media"); assert_eq!(count_example_items(&db.pool).await, SEEDED_ITEMS); assert_eq!( count_discover_visible_example_items(&db.pool).await, SEEDED_ITEMS ); } // ── Harness phase ── // // The accounts and OAuth client the mt browser axis logs in with. Their whole // value is that a testnot reseed reproduces them exactly: an audit run that // cannot log in stops at reads, which is the coverage gap the phase exists to // close. These tests pin the three things a login actually depends on, the // password verifying, the perks MNW will report, and the redirect URI being // registered, plus survival across a reseed. use makenotwork::seed::harness::{ self, CREATOR_ACCOUNT_ID, FAN_ACCOUNT_ID, HarnessOptions, OWNER_ACCOUNT_ID, }; const HARNESS_REDIRECT_URI: &str = "http://mt.example.test:3400/auth/callback"; fn harness_opts() -> HarnessOptions { HarnessOptions { password: "harness-test-password".to_string(), redirect_uri: HARNESS_REDIRECT_URI.to_string(), } } /// Options that run the catalog *and* the harness phase, as a testnot box with /// both env vars set does. fn testnot_opts_with_harness() -> SeedOptions { SeedOptions { harness: Some(harness_opts()), ..testnot_opts() } } pub(super) async fn password_hash_of(pool: &sqlx::PgPool, id: uuid::Uuid) -> String { sqlx::query_scalar("SELECT password_hash FROM users WHERE id = $1") .bind(id) .fetch_one(pool) .await .expect("harness account should exist") } #[tokio::test] async fn harness_accounts_can_log_in_and_carry_their_perks() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts_with_harness(), &SeedMedia::none()) .await .expect("seed with harness phase"); // The password verifies: this is the login the browser run performs. for id in [FAN_ACCOUNT_ID, CREATOR_ACCOUNT_ID, OWNER_ACCOUNT_ID] { let hash = password_hash_of(&db.pool, id).await; assert!( makenotwork::auth::verify_password_async(harness_opts().password, hash) .await .expect("verify"), "seeded password should verify for {id}" ); } // Perks, as `/oauth/userinfo` computes them: fan_plus from an active // subscription row, is_creator from a non-null creator_tier. The pair is // what mt's `UserPerks::effective_plus` gate reads, and the two accounts // exercise its two halves separately. let fan_plus_active: bool = sqlx::query_scalar( "SELECT EXISTS(SELECT 1 FROM fan_plus_subscriptions \ WHERE user_id = $1 AND status = 'active')", ) .bind(FAN_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("fan plus lookup"); assert!(fan_plus_active, "fan account should hold active Fan+"); let tiers: Vec> = sqlx::query_scalar("SELECT creator_tier FROM users WHERE id = ANY($1) ORDER BY username") .bind(vec![FAN_ACCOUNT_ID, CREATOR_ACCOUNT_ID, OWNER_ACCOUNT_ID]) .fetch_all(&db.pool) .await .expect("creator tiers"); // Ordered by username: harness_creator, harness_fan, harness_owner. assert_eq!( tiers, vec![Some("everything".to_string()), None, None], "only the creator account should report is_creator" ); // Creator powers follow the tier. Three accounts that all hold them would be // one role tested three times. let can_create: Vec = sqlx::query_scalar( "SELECT can_create_projects FROM users WHERE id = ANY($1) ORDER BY username", ) .bind(vec![FAN_ACCOUNT_ID, CREATOR_ACCOUNT_ID, OWNER_ACCOUNT_ID]) .fetch_all(&db.pool) .await .expect("project rights"); assert_eq!(can_create, vec![true, false, false]); // Not locked out, not suspended: a prior run's failed logins must not // survive the reset. let clean: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM users WHERE id = ANY($1) \ AND failed_login_attempts = 0 AND locked_until IS NULL \ AND suspended_at IS NULL AND deactivated_at IS NULL", ) .bind(vec![FAN_ACCOUNT_ID, CREATOR_ACCOUNT_ID, OWNER_ACCOUNT_ID]) .fetch_one(&db.pool) .await .expect("account state"); assert_eq!(clean, 3); } #[tokio::test] async fn harness_client_registers_its_redirect_uri() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts_with_harness(), &SeedMedia::none()) .await .expect("seed with harness phase"); // `validate_redirect_uri` waves through localhost only; the harness instance // is reached over the tailnet, so the URI has to be registered or the // authorize call fails with "redirect_uri is not allowed". let row: (uuid::Uuid, Vec, bool) = sqlx::query_as( "SELECT creator_id, redirect_uris, is_active FROM sync_apps WHERE api_key_hash = $1", ) .bind(makenotwork::db::synckit::hash_api_key(harness::CLIENT_ID)) .fetch_one(&db.pool) .await .expect("harness OAuth client should be registered"); assert_eq!(row.0, CREATOR_ACCOUNT_ID, "client is owned by the creator"); assert_eq!(row.1, vec![HARNESS_REDIRECT_URI.to_string()]); assert!(row.2, "client must be active"); } #[tokio::test] async fn harness_survives_a_reseed_with_stable_ids() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts_with_harness(), &SeedMedia::none()) .await .expect("first seed"); // A reseed wipes every @example.test account first, harness accounts // included, then rebuilds them. Stable ids are the property mt's seed // depends on: it pre-assigns community roles by mnw_account_id, so an id // that moved would leave the harness a plain member with no way to moderate. seed::run(&db.pool, &testnot_opts_with_harness(), &SeedMedia::none()) .await .expect("reseed"); let ids: Vec = sqlx::query_scalar( "SELECT id FROM users WHERE username LIKE 'harness\\_%' ORDER BY username", ) .fetch_all(&db.pool) .await .expect("harness ids"); assert_eq!( ids, vec![CREATOR_ACCOUNT_ID, FAN_ACCOUNT_ID, OWNER_ACCOUNT_ID], "harness ids must be identical after a reseed" ); // And the catalog is unchanged by the phase running twice. assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS + 3); } #[tokio::test] async fn without_harness_options_the_phase_does_not_run() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &SeedMedia::none()) .await .expect("seed without harness"); let harness_accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE username LIKE 'harness\\_%'") .fetch_one(&db.pool) .await .expect("count"); assert_eq!(harness_accounts, 0, "harness phase must be opt-in"); assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS); }