Skip to main content

max / multithreaded

6.1 KB · 207 lines History Blame Raw
1 use crate::harness::TestHarness;
2 use axum::http::StatusCode;
3
4 #[tokio::test]
5 async fn unauthenticated_sees_login_link() {
6 let mut h = TestHarness::new().await;
7 let resp = h.client.get("/").await;
8
9 assert!(resp.status.is_success());
10 assert!(
11 resp.text.contains("Login"),
12 "Expected 'Login' link in unauthenticated page"
13 );
14 }
15
16 #[tokio::test]
17 async fn login_redirects_to_mnw() {
18 let mut h = TestHarness::new().await;
19 let resp = h.client.get("/auth/login").await;
20
21 // Should redirect to the MNW OAuth authorize endpoint
22 assert!(
23 resp.status.is_redirection(),
24 "Expected redirect, got {}",
25 resp.status
26 );
27 }
28
29 #[tokio::test]
30 async fn logout_clears_session() {
31 let mut h = TestHarness::new().await;
32 let user_id = h.login_as("logouttest").await;
33 let comm_id = h.create_community("Test", "test").await;
34 let _cat_id = h.create_category(comm_id, "General", "general").await;
35 h.add_membership(user_id, comm_id, "member").await;
36
37 // Verify logged in — page shows username
38 let resp = h.client.get("/").await;
39 assert!(resp.text.contains("logouttest"));
40
41 // Logout (POST)
42 h.client.post_form("/auth/logout", "").await;
43
44 // Should show Login link again
45 let resp = h.client.get("/").await;
46 assert!(
47 resp.text.contains("Login"),
48 "Expected 'Login' link after logout"
49 );
50 }
51
52 #[tokio::test]
53 async fn login_redirect_includes_pkce_and_state() {
54 let mut h = TestHarness::new().await;
55 let resp = h.client.get("/auth/login").await;
56
57 assert!(resp.status.is_redirection());
58 let location = resp
59 .headers
60 .get("location")
61 .and_then(|v| v.to_str().ok())
62 .expect("redirect should have location header");
63
64 assert!(
65 location.contains("client_id=test-client-id"),
66 "URL should contain client_id"
67 );
68 assert!(
69 location.contains("code_challenge="),
70 "URL should contain code_challenge"
71 );
72 assert!(
73 location.contains("code_challenge_method=S256"),
74 "URL should contain S256 method"
75 );
76 assert!(
77 location.contains("state="),
78 "URL should contain state parameter"
79 );
80 assert!(
81 location.contains("response_type=code"),
82 "URL should contain response_type=code"
83 );
84 assert!(
85 location.starts_with("http://127.0.0.1:9999/oauth/authorize"),
86 "Should redirect to MNW OAuth endpoint"
87 );
88 }
89
90 #[tokio::test]
91 async fn callback_without_prior_login_rejects_state() {
92 let mut h = TestHarness::new().await;
93 // Establish session without going through login
94 h.client.get("/").await;
95
96 // Call callback directly — session has no stored state
97 let resp = h
98 .client
99 .get("/auth/callback?code=fake&state=somestate")
100 .await;
101
102 assert!(resp.status.is_redirection());
103 let location = resp
104 .headers
105 .get("location")
106 .and_then(|v| v.to_str().ok())
107 .expect("should have location header");
108 assert!(
109 location.contains("error=state_mismatch"),
110 "Should redirect with state_mismatch error, got: {}",
111 location
112 );
113 }
114
115 #[tokio::test]
116 async fn callback_with_wrong_state_rejects() {
117 let mut h = TestHarness::new().await;
118 // Login sets state + PKCE verifier in session
119 h.client.get("/auth/login").await;
120
121 // Call callback with wrong state
122 let resp = h
123 .client
124 .get("/auth/callback?code=fake&state=wrong_state_value")
125 .await;
126
127 assert!(resp.status.is_redirection());
128 let location = resp
129 .headers
130 .get("location")
131 .and_then(|v| v.to_str().ok())
132 .expect("should have location header");
133 assert!(
134 location.contains("error=state_mismatch"),
135 "Should redirect with state_mismatch error, got: {}",
136 location
137 );
138 }
139
140 #[tokio::test]
141 async fn callback_with_correct_state_fails_at_token_exchange() {
142 let mut h = TestHarness::new().await;
143
144 // Login to set state in session
145 let login_resp = h.client.get("/auth/login").await;
146 let location = login_resp
147 .headers
148 .get("location")
149 .and_then(|v| v.to_str().ok())
150 .expect("login should redirect");
151
152 // Extract state from redirect URL
153 let state_start = location.find("state=").expect("state in URL") + 6;
154 let state_end = location[state_start..]
155 .find('&')
156 .map(|i| state_start + i)
157 .unwrap_or(location.len());
158 let state = &location[state_start..state_end];
159
160 // Call callback with correct state — will try HTTP to 127.0.0.1:9999 (no server)
161 let resp = h
162 .client
163 .get(&format!("/auth/callback?code=fake&state={}", state))
164 .await;
165
166 assert!(resp.status.is_redirection());
167 let cb_location = resp
168 .headers
169 .get("location")
170 .and_then(|v| v.to_str().ok())
171 .expect("should have location header");
172 assert!(
173 cb_location.contains("error=token_request_failed"),
174 "Should fail at token exchange, got: {}",
175 cb_location
176 );
177 }
178
179 #[tokio::test]
180 async fn suspended_user_sees_error_page() {
181 let mut h = TestHarness::new().await;
182 let user_id = h.login_as("suspendeduser").await;
183 let comm_id = h.create_community("Test", "test").await;
184 let _cat_id = h.create_category(comm_id, "General", "general").await;
185 h.add_membership(user_id, comm_id, "member").await;
186
187 // Verify can access community page while not suspended
188 let resp = h.client.get("/p/test").await;
189 assert_eq!(resp.status, StatusCode::OK);
190
191 // Suspend the user
192 sqlx::query("UPDATE users SET suspended_at = now(), suspension_reason = 'test' WHERE mnw_account_id = $1")
193 .bind(user_id)
194 .execute(&h.db)
195 .await
196 .unwrap();
197
198 // Suspended user can still browse with existing session (suspension blocks new logins)
199 // but verify the page still loads (no crash)
200 let resp = h.client.get("/p/test").await;
201 assert!(
202 resp.status == StatusCode::OK || resp.status == StatusCode::FORBIDDEN,
203 "Should either allow (existing session) or block, got: {}",
204 resp.status
205 );
206 }
207