Skip to main content

max / makenotwork

8.3 KB · 244 lines History Blame Raw
1 //! DB-layer contract tests for the account core (`db::users`).
2 //!
3 //! `users.rs` is the widest CRUD/lookup module and its contracts were asserted
4 //! only indirectly through HTTP flows (auth, dashboard, admin), audit Run 18
5 //! Testing. These call the `db::users::` functions directly so the invariants
6 //! the account surface leans on are pinned at the layer they live in: lookup
7 //! round-trip (found vs not-found), exact-vs-normalized key matching for
8 //! username/email, the creator-permission flag, the voluntary creator-pause
9 //! toggle, and the Stripe-webhook status write keyed by connected account.
10
11 use crate::harness::TestHarness;
12 use makenotwork::db::{self, Email, Username};
13
14 // ── lookup round-trip: found vs not-found ──
15
16 #[tokio::test]
17 async fn lookups_round_trip_after_signup() {
18 let mut h = TestHarness::new().await;
19 let user_id = h
20 .signup("lookup_rt", "lookup_rt@test.com", "password123")
21 .await;
22
23 // by id
24 let by_id = db::users::get_user_by_id(&h.db, user_id)
25 .await
26 .expect("get_user_by_id ok")
27 .expect("signed-up user is found by id");
28 assert_eq!(by_id.id, user_id);
29 assert_eq!(by_id.username.as_str(), "lookup_rt");
30 assert_eq!(by_id.email.as_str(), "lookup_rt@test.com");
31
32 // by username (exact match)
33 let username = Username::new("lookup_rt").expect("valid username");
34 let by_username = db::users::get_user_by_username(&h.db, &username)
35 .await
36 .expect("get_user_by_username ok")
37 .expect("signed-up user is found by username");
38 assert_eq!(by_username.id, user_id);
39
40 // by email (normalized match)
41 let email = Email::new("lookup_rt@test.com").expect("valid email");
42 let by_email = db::users::get_user_by_email(&h.db, &email)
43 .await
44 .expect("get_user_by_email ok")
45 .expect("signed-up user is found by email");
46 assert_eq!(by_email.id, user_id);
47 }
48
49 #[tokio::test]
50 async fn lookups_return_none_when_absent() {
51 let h = TestHarness::new().await;
52
53 // A random UUID that was never inserted.
54 let missing_id = db::UserId::new();
55 assert!(
56 db::users::get_user_by_id(&h.db, missing_id)
57 .await
58 .expect("get_user_by_id ok")
59 .is_none(),
60 "an unknown id resolves to None, not an error"
61 );
62
63 let missing_username = Username::new("nobody_here").expect("valid username");
64 assert!(
65 db::users::get_user_by_username(&h.db, &missing_username)
66 .await
67 .expect("get_user_by_username ok")
68 .is_none(),
69 "an unknown username resolves to None"
70 );
71
72 let missing_email = Email::new("nobody@test.com").expect("valid email");
73 assert!(
74 db::users::get_user_by_email(&h.db, &missing_email)
75 .await
76 .expect("get_user_by_email ok")
77 .is_none(),
78 "an unknown email resolves to None"
79 );
80 }
81
82 // ── key matching: username is exact, email is normalized ──
83
84 #[tokio::test]
85 async fn username_lookup_is_case_exact() {
86 let mut h = TestHarness::new().await;
87 // Signup stores the username verbatim (no lowercasing); the lookup SQL is
88 // `WHERE username = $1`, so a differently-cased handle must NOT collide.
89 let user_id = h
90 .signup("CaseUser", "caseuser@test.com", "password123")
91 .await;
92
93 let exact = Username::new("CaseUser").expect("valid username");
94 let found = db::users::get_user_by_username(&h.db, &exact)
95 .await
96 .expect("get_user_by_username ok")
97 .expect("exact-case username is found");
98 assert_eq!(found.id, user_id);
99
100 let wrong_case = Username::new("caseuser").expect("valid username");
101 assert!(
102 db::users::get_user_by_username(&h.db, &wrong_case)
103 .await
104 .expect("get_user_by_username ok")
105 .is_none(),
106 "username match is case-exact, so a lowercased variant does not resolve"
107 );
108 }
109
110 #[tokio::test]
111 async fn email_lookup_is_case_insensitive_by_normalization() {
112 let mut h = TestHarness::new().await;
113 let user_id = h
114 .signup("email_norm", "email_norm@test.com", "password123")
115 .await;
116
117 // `Email::new` trims + lowercases, so a mixed-case spelling normalizes to
118 // the same stored value and still resolves to the same row.
119 let mixed = Email::new("Email_Norm@Test.Com").expect("valid email");
120 let found = db::users::get_user_by_email(&h.db, &mixed)
121 .await
122 .expect("get_user_by_email ok")
123 .expect("mixed-case email normalizes and is found");
124 assert_eq!(found.id, user_id);
125 }
126
127 // ── creator permission flag ──
128
129 #[tokio::test]
130 async fn grant_creator_flips_the_creator_flag() {
131 let mut h = TestHarness::new().await;
132 let user_id = h
133 .signup("creator_flag", "creator_flag@test.com", "password123")
134 .await;
135
136 let before = db::users::get_user_by_id(&h.db, user_id)
137 .await
138 .expect("get_user_by_id ok")
139 .expect("user found");
140 assert!(
141 !before.can_create_projects,
142 "a fresh signup cannot create projects"
143 );
144
145 h.grant_creator(user_id).await;
146
147 let after = db::users::get_user_by_id(&h.db, user_id)
148 .await
149 .expect("get_user_by_id ok")
150 .expect("user found");
151 assert!(
152 after.can_create_projects,
153 "granting creator access sets can_create_projects"
154 );
155 }
156
157 // ── voluntary creator pause toggle ──
158
159 #[tokio::test]
160 async fn pause_and_unpause_creator_toggle_the_flag() {
161 let mut h = TestHarness::new().await;
162 let user_id = h.create_creator("pause_toggle").await;
163
164 let fresh = db::users::get_user_by_id(&h.db, user_id)
165 .await
166 .expect("get_user_by_id ok")
167 .expect("user found");
168 assert!(!fresh.is_creator_paused(), "a new creator is not paused");
169
170 db::users::pause_creator(&h.db, user_id)
171 .await
172 .expect("pause_creator ok");
173 let paused = db::users::get_user_by_id(&h.db, user_id)
174 .await
175 .expect("get_user_by_id ok")
176 .expect("user found");
177 assert!(
178 paused.is_creator_paused(),
179 "pause_creator stamps creator_paused_at"
180 );
181
182 db::users::unpause_creator(&h.db, user_id)
183 .await
184 .expect("unpause_creator ok");
185 let resumed = db::users::get_user_by_id(&h.db, user_id)
186 .await
187 .expect("get_user_by_id ok")
188 .expect("user found");
189 assert!(
190 !resumed.is_creator_paused(),
191 "unpause_creator clears creator_paused_at"
192 );
193 }
194
195 // ── Stripe webhook status write (keyed by connected account) ──
196
197 #[tokio::test]
198 async fn update_user_stripe_status_persists_flags_by_account() {
199 let mut h = TestHarness::new().await;
200 let user_id = h.create_creator("stripe_status").await;
201 // connect_stripe sets stripe_account_id and flips all three Stripe flags on.
202 h.connect_stripe(user_id, "acct_dbul_status").await;
203
204 // The webhook write finds the row by stripe_account_id and returns it.
205 let updated = db::users::update_user_stripe_status(
206 &h.db,
207 "acct_dbul_status",
208 true, // onboarding_complete
209 false, // payouts_enabled
210 true, // charges_enabled
211 )
212 .await
213 .expect("update_user_stripe_status ok")
214 .expect("a matching connected account returns the updated row");
215 assert_eq!(updated.id, user_id);
216 assert!(updated.stripe_onboarding_complete);
217 assert!(!updated.stripe_payouts_enabled);
218 assert!(updated.stripe_charges_enabled);
219
220 // The flags are durable, not just echoed by the RETURNING clause.
221 let reread = db::users::get_user_by_id(&h.db, user_id)
222 .await
223 .expect("get_user_by_id ok")
224 .expect("user found");
225 assert!(reread.stripe_onboarding_complete);
226 assert!(!reread.stripe_payouts_enabled);
227 assert!(reread.stripe_charges_enabled);
228 }
229
230 #[tokio::test]
231 async fn update_user_stripe_status_is_noop_for_unknown_account() {
232 let h = TestHarness::new().await;
233 // No user carries this account id, so the UPDATE matches nothing and the
234 // webhook handler gets None (rather than an error) to ignore the event.
235 let result =
236 db::users::update_user_stripe_status(&h.db, "acct_dbul_nonexistent", true, true, true)
237 .await
238 .expect("update_user_stripe_status ok");
239 assert!(
240 result.is_none(),
241 "an unmatched stripe_account_id is a no-op returning None"
242 );
243 }
244