Skip to main content

max / makenotwork

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