use crate::harness::TestHarness; use axum::http::StatusCode; #[tokio::test] async fn unauthenticated_sees_login_link() { let mut h = TestHarness::new().await; let resp = h.client.get("/").await; assert!(resp.status.is_success()); assert!( resp.text.contains("Login"), "Expected 'Login' link in unauthenticated page" ); } #[tokio::test] async fn login_redirects_to_mnw() { let mut h = TestHarness::new().await; let resp = h.client.get("/auth/login").await; // Should redirect to the MNW OAuth authorize endpoint assert!( resp.status.is_redirection(), "Expected redirect, got {}", resp.status ); } #[tokio::test] async fn logout_clears_session() { let mut h = TestHarness::new().await; let user_id = h.login_as("logouttest").await; let comm_id = h.create_community("Test", "test").await; let _cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; // Verify logged in — page shows username let resp = h.client.get("/").await; assert!(resp.text.contains("logouttest")); // Logout (POST) h.client.post_form("/auth/logout", "").await; // Should show Login link again let resp = h.client.get("/").await; assert!( resp.text.contains("Login"), "Expected 'Login' link after logout" ); } #[tokio::test] async fn login_redirect_includes_pkce_and_state() { let mut h = TestHarness::new().await; let resp = h.client.get("/auth/login").await; assert!(resp.status.is_redirection()); let location = resp .headers .get("location") .and_then(|v| v.to_str().ok()) .expect("redirect should have location header"); assert!( location.contains("client_id=test-client-id"), "URL should contain client_id" ); assert!( location.contains("code_challenge="), "URL should contain code_challenge" ); assert!( location.contains("code_challenge_method=S256"), "URL should contain S256 method" ); assert!( location.contains("state="), "URL should contain state parameter" ); assert!( location.contains("response_type=code"), "URL should contain response_type=code" ); assert!( location.starts_with("http://127.0.0.1:9999/oauth/authorize"), "Should redirect to MNW OAuth endpoint" ); } #[tokio::test] async fn callback_without_prior_login_rejects_state() { let mut h = TestHarness::new().await; // Establish session without going through login h.client.get("/").await; // Call callback directly — session has no stored state let resp = h .client .get("/auth/callback?code=fake&state=somestate") .await; assert!(resp.status.is_redirection()); let location = resp .headers .get("location") .and_then(|v| v.to_str().ok()) .expect("should have location header"); assert!( location.contains("error=state_mismatch"), "Should redirect with state_mismatch error, got: {}", location ); } #[tokio::test] async fn callback_with_wrong_state_rejects() { let mut h = TestHarness::new().await; // Login sets state + PKCE verifier in session h.client.get("/auth/login").await; // Call callback with wrong state let resp = h .client .get("/auth/callback?code=fake&state=wrong_state_value") .await; assert!(resp.status.is_redirection()); let location = resp .headers .get("location") .and_then(|v| v.to_str().ok()) .expect("should have location header"); assert!( location.contains("error=state_mismatch"), "Should redirect with state_mismatch error, got: {}", location ); } #[tokio::test] async fn callback_with_correct_state_fails_at_token_exchange() { let mut h = TestHarness::new().await; // Login to set state in session let login_resp = h.client.get("/auth/login").await; let location = login_resp .headers .get("location") .and_then(|v| v.to_str().ok()) .expect("login should redirect"); // Extract state from redirect URL let state_start = location.find("state=").expect("state in URL") + 6; let state_end = location[state_start..] .find('&') .map(|i| state_start + i) .unwrap_or(location.len()); let state = &location[state_start..state_end]; // Call callback with correct state — will try HTTP to 127.0.0.1:9999 (no server) let resp = h .client .get(&format!("/auth/callback?code=fake&state={}", state)) .await; assert!(resp.status.is_redirection()); let cb_location = resp .headers .get("location") .and_then(|v| v.to_str().ok()) .expect("should have location header"); assert!( cb_location.contains("error=token_request_failed"), "Should fail at token exchange, got: {}", cb_location ); } #[tokio::test] async fn suspended_user_sees_error_page() { let mut h = TestHarness::new().await; let user_id = h.login_as("suspendeduser").await; let comm_id = h.create_community("Test", "test").await; let _cat_id = h.create_category(comm_id, "General", "general").await; h.add_membership(user_id, comm_id, "member").await; // Verify can access community page while not suspended let resp = h.client.get("/p/test").await; assert_eq!(resp.status, StatusCode::OK); // Suspend the user sqlx::query("UPDATE users SET suspended_at = now(), suspension_reason = 'test' WHERE mnw_account_id = $1") .bind(user_id) .execute(&h.db) .await .unwrap(); // Suspended user can still browse with existing session (suspension blocks new logins) // but verify the page still loads (no crash) let resp = h.client.get("/p/test").await; assert!( resp.status == StatusCode::OK || resp.status == StatusCode::FORBIDDEN, "Should either allow (existing session) or block, got: {}", resp.status ); }