Skip to main content

max / makenotwork

9.8 KB · 314 lines History Blame Raw
1 //! Auth workflow: signup -> dashboard -> logout -> dashboard (redirect)
2
3 use crate::harness::TestHarness;
4
5 #[tokio::test]
6 async fn multibyte_password_register_then_login() {
7 // Regression (audit Run 22): signup capped password length by char count
8 // (`chars().count()`) while login/OAuth/SyncKit capped by BYTE count
9 // (`.len()`). A password of <=128 chars but >128 bytes, any multibyte
10 // passphrase near the cap, registered fine, then was silently rejected at
11 // every subsequent login: a permanent, undiagnosable self-lockout.
12 //
13 // 65 CJK chars = 65 chars (well under the 128-char signup cap) = 195 bytes
14 // (over the old 128-byte login cap). Under the bug, signup succeeds but the
15 // login below never establishes a session.
16 let password = "\u{4f60}".repeat(65); // 你 x65
17 assert_eq!(password.chars().count(), 65);
18 assert!(password.len() > 128, "test password must exceed 128 bytes");
19
20 let mut h = TestHarness::new().await;
21 let _user_id = h.signup("mb_user", "mb@example.com", &password).await;
22 h.client.post_form("/logout", "").await;
23
24 // Log back in with the identical multibyte password.
25 h.login("mb_user", &password).await;
26
27 // A failed login renders 200 with an error but sets no session, so the
28 // definitive check is that the authenticated dashboard is reachable.
29 let resp = h.client.get("/dashboard").await;
30 assert_eq!(
31 resp.status, 200,
32 "multibyte-password account must be able to log back in"
33 );
34 }
35
36 #[tokio::test]
37 async fn signup_login_logout_flow() {
38 let mut h = TestHarness::new().await;
39
40 // Sign up
41 let _user_id = h
42 .signup("testuser", "test@example.com", "password123")
43 .await;
44
45 // Should be logged in, dashboard returns 200
46 let resp = h.client.get("/dashboard").await;
47 assert_eq!(
48 resp.status, 200,
49 "Dashboard should be accessible after signup"
50 );
51
52 let resp = h.client.post_form("/logout", "").await;
53 assert!(
54 resp.status.is_success() || resp.status.is_redirection(),
55 "Logout should succeed"
56 );
57
58 // Dashboard should now redirect (302) or return 401
59 let resp = h.client.get("/dashboard").await;
60 assert!(
61 resp.status == 302 || resp.status == 303 || resp.status == 401,
62 "Dashboard should redirect after logout, got {}",
63 resp.status
64 );
65 }
66
67 #[tokio::test]
68 async fn signup_with_taken_email_does_not_reveal_or_create() {
69 // m1 (ultra-fuzz Run 4): a signup attempt whose EMAIL is already registered
70 // must not return "this email is already registered" (an account-existence
71 // oracle for a private identifier). It returns the same step-2 response a
72 // fresh signup returns and creates no second account; the real owner is
73 // reached out-of-band by the "account exists" email.
74 let mut h = TestHarness::new().await;
75 h.signup("owner", "owner@example.com", "password123").await;
76 h.client.post_form("/logout", "").await;
77
78 // A new username, but the same (taken) email.
79 h.client.fetch_csrf_token().await;
80 let body = "username=intruder&email=owner@example.com&password=password123";
81 let resp = h.client.post_form("/join/step/account", body).await;
82
83 assert!(
84 resp.status.is_success(),
85 "taken-email signup should not error: {}",
86 resp.status
87 );
88 let lower = resp.text.to_lowercase();
89 assert!(
90 !lower.contains("already registered") && !lower.contains("already exists"),
91 "response must not reveal the email is registered: {}",
92 resp.text
93 );
94
95 // No second account created for the probed email.
96 let count: i64 =
97 sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE email = 'owner@example.com'")
98 .fetch_one(&h.db)
99 .await
100 .unwrap();
101 assert_eq!(
102 count, 1,
103 "no duplicate account may be created for a taken email"
104 );
105 // And the probed username was never registered.
106 let intruder: i64 =
107 sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE username = 'intruder'")
108 .fetch_one(&h.db)
109 .await
110 .unwrap();
111 assert_eq!(
112 intruder, 0,
113 "no account should be created on the taken-email path"
114 );
115 }
116
117 #[tokio::test]
118 async fn login_with_existing_account() {
119 let mut h = TestHarness::new().await;
120
121 // Sign up and then log out
122 let _user_id = h
123 .signup("alice", "alice@example.com", "secure_pass99")
124 .await;
125 h.client.post_form("/logout", "").await;
126
127 // Log back in
128 h.login("alice", "secure_pass99").await;
129
130 // Dashboard should be accessible
131 let resp = h.client.get("/dashboard").await;
132 assert_eq!(
133 resp.status, 200,
134 "Dashboard should be accessible after login"
135 );
136 }
137
138 #[tokio::test]
139 async fn wrong_password_rejected() {
140 let mut h = TestHarness::new().await;
141 let _user_id = h.signup("wp_user", "wp@example.com", "correctpass1").await;
142 h.client.post_form("/logout", "").await;
143
144 let resp = h
145 .client
146 .post_form("/login", "login=wp_user&password=totallyWrong")
147 .await;
148 assert!(
149 resp.status != 200 && resp.status != 303,
150 "Wrong password should not yield 200 or 303, got {}",
151 resp.status
152 );
153 }
154
155 #[tokio::test]
156 async fn nonexistent_user_rejected() {
157 let mut h = TestHarness::new().await;
158
159 let resp = h
160 .client
161 .post_form("/login", "login=ghost_user_xyz&password=anypass123")
162 .await;
163 assert!(
164 resp.status != 200 && resp.status != 303,
165 "Nonexistent user login should not yield 200 or 303, got {}",
166 resp.status
167 );
168 }
169
170 #[tokio::test]
171 async fn duplicate_email_rejected() {
172 let mut h = TestHarness::new().await;
173 let _user_id = h
174 .signup("orig_user", "dupe@example.com", "password123")
175 .await;
176 h.client.post_form("/logout", "").await;
177
178 // Attempt signup with the same email but a different username
179 let resp = h
180 .client
181 .post_form(
182 "/join",
183 "username=other_user&email=dupe@example.com&password=password123&password_confirm=password123",
184 )
185 .await;
186 assert!(
187 resp.status.is_client_error()
188 || resp.text.contains("already")
189 || resp.text.contains("taken"),
190 "Duplicate email signup should fail: {} {}",
191 resp.status,
192 resp.text
193 );
194 }
195
196 #[tokio::test]
197 async fn duplicate_username_rejected() {
198 let mut h = TestHarness::new().await;
199 let _user_id = h
200 .signup("taken_name", "first@example.com", "password123")
201 .await;
202 h.client.post_form("/logout", "").await;
203
204 // Attempt signup with the same username but a different email
205 let resp = h
206 .client
207 .post_form(
208 "/join",
209 "username=taken_name&email=second@example.com&password=password123&password_confirm=password123",
210 )
211 .await;
212 assert!(
213 resp.status.is_client_error()
214 || resp.text.contains("already")
215 || resp.text.contains("taken"),
216 "Duplicate username signup should fail: {} {}",
217 resp.status,
218 resp.text
219 );
220 }
221
222 #[tokio::test]
223 async fn login_with_email() {
224 let mut h = TestHarness::new().await;
225 let _user_id = h
226 .signup("emaillogin", "emaillogin@example.com", "password123")
227 .await;
228 h.client.post_form("/logout", "").await;
229
230 // Login using email address instead of username
231 h.login("emaillogin@example.com", "password123").await;
232
233 let resp = h.client.get("/dashboard").await;
234 assert_eq!(
235 resp.status, 200,
236 "Dashboard should be accessible after login with email"
237 );
238 }
239
240 #[tokio::test]
241 async fn password_change_flow() {
242 let mut h = TestHarness::new().await;
243 let _user_id = h
244 .signup("pwchange", "pwchange@example.com", "oldpass123")
245 .await;
246
247 // Change password via PUT form
248 let resp = h
249 .client
250 .put_form(
251 "/api/users/me/password",
252 "current_password=oldpass123&new_password=newpass456",
253 )
254 .await;
255 assert!(
256 resp.status.is_success(),
257 "Password change should succeed: {} {}",
258 resp.status,
259 resp.text
260 );
261
262 h.client.post_form("/logout", "").await;
263
264 // Login with new password should succeed
265 h.login("pwchange", "newpass456").await;
266 let resp = h.client.get("/dashboard").await;
267 assert_eq!(
268 resp.status, 200,
269 "New password should grant dashboard access"
270 );
271
272 // Logout and try old password
273 h.client.post_form("/logout", "").await;
274 let resp = h
275 .client
276 .post_form("/login", "login=pwchange&password=oldpass123")
277 .await;
278 assert!(
279 resp.status != 200 && resp.status != 303,
280 "Old password should no longer work, got {}",
281 resp.status
282 );
283 }
284
285 #[tokio::test]
286 async fn lockout_after_failed_attempts() {
287 let mut h = TestHarness::new().await;
288 let _user_id = h.signup("lockme", "lockme@example.com", "rightpass1").await;
289 h.client.post_form("/logout", "").await;
290
291 // 5 failed login attempts. `/login` is Manual-CSRF now (Phase 2), so
292 // each attempt must refresh the token, the helper handles that.
293 for _ in 0..5 {
294 h.failed_login_attempt("lockme", "wrongwrong").await;
295 }
296
297 // Now try with correct password, should be locked out. login_handler
298 // returns 200 with the form re-rendered + inline "Account is locked"
299 // message (same UX convention as the inline-error pattern); a successful
300 // login would be a 303 redirect, so absence of redirect + body containing
301 // "locked" together prove lockout.
302 let resp = h.failed_login_attempt("lockme", "rightpass1").await;
303 assert!(
304 !resp.status.is_redirection(),
305 "Lockout should not redirect to dashboard, got {}",
306 resp.status
307 );
308 assert!(
309 resp.text.to_lowercase().contains("locked"),
310 "Response should mention lockout: {}",
311 resp.text
312 );
313 }
314