Skip to main content

max / makenotwork

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