Skip to main content

max / makenotwork

8.5 KB · 230 lines History Blame Raw
1 //! DB-layer contract tests for the demo-buyer seed phase (`seed::buyer`).
2 //!
3 //! One login-capable account with a purchase history, so the landing carousel's
4 //! third frame has a library to photograph. Opt-in per box, like the harness
5 //! phase. GoingsOn 839a8e5a, option B.
6 //!
7 //! Split out of `seed_examples` on 2026-08-07: the buyer phase added 409 lines
8 //! and pushed that module past the 800-line ratchet in `test_hygiene`. The
9 //! fixtures the two share stay there, since the catalog is what the buyer buys.
10
11 use crate::harness::db::TestDb;
12 use makenotwork::seed::buyer::{self, BUYER_ACCOUNT_ID};
13 use makenotwork::seed::{self, SeedOptions};
14
15 use super::seed_examples::{
16 SEEDED_CREATORS, count_example_creators, count_example_items, discover_visible_example_slugs,
17 media_ctx, password_hash_of, testnot_opts,
18 };
19
20 /// The demo buyer's password for a test run. Not a secret here; on testnot it
21 /// comes from the box's EnvironmentFile.
22 const BUYER_PASSWORD: &str = "demo-buyer-test-password";
23
24 /// Purchases seeded for the demo buyer (see `seed::buyer::PURCHASES`).
25 const BUYER_PURCHASES: i64 = 9;
26
27 fn buyer_opts() -> buyer::BuyerOptions {
28 buyer::BuyerOptions {
29 password: BUYER_PASSWORD.to_string(),
30 }
31 }
32
33 fn testnot_opts_with_buyer() -> SeedOptions {
34 SeedOptions {
35 buyer: Some(buyer_opts()),
36 ..testnot_opts()
37 }
38 }
39
40 async fn buyer_purchase_count(pool: &sqlx::PgPool) -> i64 {
41 sqlx::query_scalar(
42 "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND status = 'completed'",
43 )
44 .bind(BUYER_ACCOUNT_ID)
45 .fetch_one(pool)
46 .await
47 .expect("count buyer purchases")
48 }
49
50 #[tokio::test]
51 async fn demo_buyer_can_log_in_and_owns_a_library() {
52 let db = TestDb::new().await;
53 seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
54 .await
55 .expect("seed with buyer phase");
56
57 // The login the capture run performs.
58 let hash = password_hash_of(&db.pool, BUYER_ACCOUNT_ID).await;
59 assert!(
60 makenotwork::auth::verify_password_async(BUYER_PASSWORD.to_string(), hash)
61 .await
62 .expect("verify"),
63 "the demo buyer's password should verify"
64 );
65
66 // Not a sandbox account: `SessionUser::check_not_sandbox` would refuse the
67 // session, and not a creator, because the point is a buyer.
68 let (is_sandbox, can_create): (bool, bool) =
69 sqlx::query_as("SELECT is_sandbox, can_create_projects FROM users WHERE id = $1")
70 .bind(BUYER_ACCOUNT_ID)
71 .fetch_one(&db.pool)
72 .await
73 .expect("buyer account");
74 assert!(!is_sandbox, "a sandbox account cannot hold a session");
75 assert!(!can_create, "the demo buyer is a buyer");
76
77 // Every purchase landed, and the library reads them through the `purchases`
78 // view, which filters on status = 'completed'.
79 assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES);
80 let in_view: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM purchases WHERE buyer_id = $1")
81 .bind(BUYER_ACCOUNT_ID)
82 .fetch_one(&db.pool)
83 .await
84 .expect("purchases view");
85 assert_eq!(in_view, BUYER_PURCHASES, "every purchase should be visible");
86
87 // But not the whole catalog: a library holding all eleven reads as a
88 // fixture rather than as somebody's shelf. (The constant-only half of this
89 // is `seed::buyer`'s own unit test; here it is checked against the items
90 // actually in the database.)
91 let unbought = count_example_items(&db.pool).await - BUYER_PURCHASES;
92 assert!(
93 unbought > 0,
94 "the buyer should leave some of the catalog unbought"
95 );
96
97 // Platform fee is zero on every row. MNW charges 0%, so a demo receipt
98 // showing anything else would misrepresent the product.
99 let nonzero_fees: i64 = sqlx::query_scalar(
100 "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND platform_fee_cents <> 0",
101 )
102 .bind(BUYER_ACCOUNT_ID)
103 .fetch_one(&db.pool)
104 .await
105 .expect("fee check");
106 assert_eq!(nonzero_fees, 0);
107
108 // Paid rows recorded what was paid. `get_user_purchases` derives its Free
109 // badge from `amount_cents = 0`, so a paid item at zero would badge wrong.
110 let paid: i64 = sqlx::query_scalar(
111 "SELECT COUNT(*) FROM transactions t JOIN items i ON i.id = t.item_id \
112 WHERE t.buyer_id = $1 AND t.amount_cents > 0",
113 )
114 .bind(BUYER_ACCOUNT_ID)
115 .fetch_one(&db.pool)
116 .await
117 .expect("paid count");
118 assert!(
119 paid >= 4,
120 "the history should include real payments, got {paid}"
121 );
122
123 // An active subscription, so the library's subscription block is not empty.
124 let (tier, status): (String, String) = sqlx::query_as(
125 "SELECT t.name, s.status FROM subscriptions s \
126 JOIN subscription_tiers t ON t.id = s.tier_id WHERE s.subscriber_id = $1",
127 )
128 .bind(BUYER_ACCOUNT_ID)
129 .fetch_one(&db.pool)
130 .await
131 .expect("buyer subscription");
132 assert_eq!((tier.as_str(), status.as_str()), ("Patron", "active"));
133
134 // License keys for the project that sells them: one of the things the
135 // library page shows, and one of the things MNW sells.
136 let keys: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM license_keys WHERE owner_id = $1")
137 .bind(BUYER_ACCOUNT_ID)
138 .fetch_one(&db.pool)
139 .await
140 .expect("license keys");
141 assert!(keys >= 1, "a license-keyed purchase should carry a key");
142
143 // Most rows are marked downloaded, so the "new version" badge is a signal
144 // rather than the default state of every row.
145 let downloaded_items: i64 =
146 sqlx::query_scalar("SELECT COUNT(DISTINCT item_id) FROM user_downloads WHERE user_id = $1")
147 .bind(BUYER_ACCOUNT_ID)
148 .fetch_one(&db.pool)
149 .await
150 .expect("downloads");
151 assert!(
152 downloaded_items >= 1,
153 "the buyer should have downloaded something"
154 );
155 }
156
157 #[tokio::test]
158 async fn demo_buyer_history_does_not_duplicate_across_reseeds() {
159 let db = TestDb::new().await;
160 seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
161 .await
162 .expect("first seed");
163 seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
164 .await
165 .expect("reseed");
166
167 // The catalog phases get idempotency from the example-data wipe, but this
168 // account is keyed by a fixed id and is not in that set, so its history has
169 // to be cleared explicitly. Without that, every reseed doubles the library.
170 assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES);
171
172 let subs: i64 =
173 sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE subscriber_id = $1")
174 .bind(BUYER_ACCOUNT_ID)
175 .fetch_one(&db.pool)
176 .await
177 .expect("subscription count");
178 assert_eq!(subs, 1, "a reseed must not stack subscriptions");
179
180 // And the id is stable, which is what lets a stored session cookie or a
181 // scripted login survive a reset.
182 let id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE username = $1")
183 .bind("demo_collector")
184 .fetch_one(&db.pool)
185 .await
186 .expect("buyer id");
187 assert_eq!(id, BUYER_ACCOUNT_ID);
188 }
189
190 #[tokio::test]
191 async fn without_buyer_options_the_phase_does_not_run() {
192 let db = TestDb::new().await;
193 seed::run(&db.pool, &testnot_opts(), &media_ctx())
194 .await
195 .expect("seed without buyer");
196
197 let accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE id = $1")
198 .bind(BUYER_ACCOUNT_ID)
199 .fetch_one(&db.pool)
200 .await
201 .expect("count");
202 assert_eq!(accounts, 0, "the demo-buyer phase must be opt-in");
203 assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS);
204 }
205
206 #[tokio::test]
207 async fn the_demo_buyer_changes_nothing_a_visitor_can_see() {
208 let db = TestDb::new().await;
209 seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
210 .await
211 .expect("seed with buyer phase");
212
213 // Option B's boundary, asserted rather than merely documented: the buyer is
214 // a capture credential, not a demo surface. It owns no project, so it never
215 // appears on /discover or /creators, and it publishes nothing.
216 let owned_projects: i64 =
217 sqlx::query_scalar("SELECT COUNT(*) FROM projects WHERE user_id = $1")
218 .bind(BUYER_ACCOUNT_ID)
219 .fetch_one(&db.pool)
220 .await
221 .expect("owned projects");
222 assert_eq!(owned_projects, 0);
223
224 // The catalog a visitor sees is exactly what it was without the phase.
225 assert_eq!(
226 discover_visible_example_slugs(&db.pool).await.len() as i64,
227 SEEDED_CREATORS
228 );
229 }
230