//! DB-layer contract tests for the demo-buyer seed phase (`seed::buyer`). //! //! One login-capable account with a purchase history, so the landing carousel's //! third frame has a library to photograph. Opt-in per box, like the harness //! phase. The fixtures this shares with `seed_examples` live there, since the //! catalog is what the buyer buys. use crate::harness::db::TestDb; use makenotwork::seed::buyer::{self, BUYER_ACCOUNT_ID}; use makenotwork::seed::{self, SeedOptions}; use super::seed_examples::{ SEEDED_CREATORS, count_example_creators, count_example_items, discover_visible_example_slugs, media_ctx, password_hash_of, testnot_opts, }; /// The demo buyer's password for a test run. Not a secret here; on testnot it /// comes from the box's EnvironmentFile. const BUYER_PASSWORD: &str = "demo-buyer-test-password"; /// Purchases seeded for the demo buyer (see `seed::buyer::PURCHASES`). const BUYER_PURCHASES: i64 = 9; fn buyer_opts() -> buyer::BuyerOptions { buyer::BuyerOptions { password: BUYER_PASSWORD.to_string(), } } fn testnot_opts_with_buyer() -> SeedOptions { SeedOptions { buyer: Some(buyer_opts()), ..testnot_opts() } } async fn buyer_purchase_count(pool: &sqlx::PgPool) -> i64 { sqlx::query_scalar( "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND status = 'completed'", ) .bind(BUYER_ACCOUNT_ID) .fetch_one(pool) .await .expect("count buyer purchases") } #[tokio::test] async fn demo_buyer_can_log_in_and_owns_a_library() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx()) .await .expect("seed with buyer phase"); // The login the capture run performs. let hash = password_hash_of(&db.pool, BUYER_ACCOUNT_ID).await; assert!( makenotwork::auth::verify_password_async(BUYER_PASSWORD.to_string(), hash) .await .expect("verify"), "the demo buyer's password should verify" ); // Not a sandbox account: `SessionUser::check_not_sandbox` would refuse the // session, and not a creator, because the point is a buyer. let (is_sandbox, can_create): (bool, bool) = sqlx::query_as("SELECT is_sandbox, can_create_projects FROM users WHERE id = $1") .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("buyer account"); assert!(!is_sandbox, "a sandbox account cannot hold a session"); assert!(!can_create, "the demo buyer is a buyer"); // Every purchase landed, and the library reads them through the `purchases` // view, which filters on status = 'completed'. assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES); let in_view: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM purchases WHERE buyer_id = $1") .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("purchases view"); assert_eq!(in_view, BUYER_PURCHASES, "every purchase should be visible"); // But not the whole catalog: a library holding all eleven reads as a // fixture rather than as somebody's shelf. (The constant-only half of this // is `seed::buyer`'s own unit test; here it is checked against the items // actually in the database.) let unbought = count_example_items(&db.pool).await - BUYER_PURCHASES; assert!( unbought > 0, "the buyer should leave some of the catalog unbought" ); // Platform fee is zero on every row. MNW charges 0%, so a demo receipt // showing anything else would misrepresent the product. let nonzero_fees: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND platform_fee_cents <> 0", ) .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("fee check"); assert_eq!(nonzero_fees, 0); // Paid rows recorded what was paid. `get_user_purchases` derives its Free // badge from `amount_cents = 0`, so a paid item at zero would badge wrong. let paid: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM transactions t JOIN items i ON i.id = t.item_id \ WHERE t.buyer_id = $1 AND t.amount_cents > 0", ) .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("paid count"); assert!( paid >= 4, "the history should include real payments, got {paid}" ); // An active subscription, so the library's subscription block is not empty. let (tier, status): (String, String) = sqlx::query_as( "SELECT t.name, s.status FROM subscriptions s \ JOIN subscription_tiers t ON t.id = s.tier_id WHERE s.subscriber_id = $1", ) .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("buyer subscription"); assert_eq!((tier.as_str(), status.as_str()), ("Patron", "active")); // License keys for the project that sells them: one of the things the // library page shows, and one of the things MNW sells. let keys: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM license_keys WHERE owner_id = $1") .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("license keys"); assert!(keys >= 1, "a license-keyed purchase should carry a key"); // Most rows are marked downloaded, so the "new version" badge is a signal // rather than the default state of every row. let downloaded_items: i64 = sqlx::query_scalar("SELECT COUNT(DISTINCT item_id) FROM user_downloads WHERE user_id = $1") .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("downloads"); assert!( downloaded_items >= 1, "the buyer should have downloaded something" ); } #[tokio::test] async fn demo_buyer_history_does_not_duplicate_across_reseeds() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx()) .await .expect("first seed"); seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx()) .await .expect("reseed"); // The catalog phases get idempotency from the example-data wipe, but this // account is keyed by a fixed id and is not in that set, so its history has // to be cleared explicitly. Without that, every reseed doubles the library. assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES); let subs: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE subscriber_id = $1") .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("subscription count"); assert_eq!(subs, 1, "a reseed must not stack subscriptions"); // And the id is stable, which is what lets a stored session cookie or a // scripted login survive a reset. let id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE username = $1") .bind("demo_collector") .fetch_one(&db.pool) .await .expect("buyer id"); assert_eq!(id, BUYER_ACCOUNT_ID); } #[tokio::test] async fn without_buyer_options_the_phase_does_not_run() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts(), &media_ctx()) .await .expect("seed without buyer"); let accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE id = $1") .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("count"); assert_eq!(accounts, 0, "the demo-buyer phase must be opt-in"); assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS); } #[tokio::test] async fn the_demo_buyer_changes_nothing_a_visitor_can_see() { let db = TestDb::new().await; seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx()) .await .expect("seed with buyer phase"); // Option B's boundary, asserted rather than merely documented: the buyer is // a capture credential, not a demo surface. It owns no project, so it never // appears on /discover or /creators, and it publishes nothing. let owned_projects: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM projects WHERE user_id = $1") .bind(BUYER_ACCOUNT_ID) .fetch_one(&db.pool) .await .expect("owned projects"); assert_eq!(owned_projects, 0); // The catalog a visitor sees is exactly what it was without the phase. assert_eq!( discover_visible_example_slugs(&db.pool).await.len() as i64, SEEDED_CREATORS ); }