mod common; use axum::http::StatusCode; use common::*; // ═══════════════════════════════════════════════════════════════════ // Registration // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn register_success() { let (app, _pool) = build_test_app().await; let (token, refresh, json) = register(&app, "alice", "alice@test.io", DEFAULT_PASSWORD).await; assert!(!token.is_empty()); assert!(!refresh.is_empty()); assert_eq!(json["account"]["username"], "alice"); assert_eq!(json["account"]["role"], "user"); assert_eq!(json["account"]["status"], "active"); } #[tokio::test] async fn register_duplicate_username() { let (app, _pool) = build_test_app().await; register(&app, "dupuser", "dup@test.io", DEFAULT_PASSWORD).await; let (status, _) = send( &app, post_public( "/api/v1/auth/register", serde_json::json!({ "username": "dupuser", "email": "other@test.io", "password": DEFAULT_PASSWORD }), ), ).await; assert_eq!(status, StatusCode::CONFLICT); } #[tokio::test] async fn register_duplicate_email() { let (app, _pool) = build_test_app().await; register(&app, "user1", "same@test.io", DEFAULT_PASSWORD).await; let (status, _) = send( &app, post_public( "/api/v1/auth/register", serde_json::json!({ "username": "user2", "email": "same@test.io", "password": DEFAULT_PASSWORD }), ), ).await; assert_eq!(status, StatusCode::CONFLICT); } #[tokio::test] async fn register_validation_short_username() { let (app, _pool) = build_test_app().await; let (status, body) = send( &app, post_public( "/api/v1/auth/register", serde_json::json!({ "username": "ab", "email": "a@b.c", "password": DEFAULT_PASSWORD }), ), ).await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(body["error"], "INVALID_INPUT"); } #[tokio::test] async fn register_validation_bad_email() { let (app, _pool) = build_test_app().await; let (status, _) = send( &app, post_public( "/api/v1/auth/register", serde_json::json!({ "username": "goodname", "email": "no-at-sign", "password": DEFAULT_PASSWORD }), ), ).await; assert_eq!(status, StatusCode::BAD_REQUEST); } #[tokio::test] async fn register_validation_short_password() { let (app, _pool) = build_test_app().await; let (status, _) = send( &app, post_public( "/api/v1/auth/register", serde_json::json!({ "username": "goodname", "email": "a@b.c", "password": "short" }), ), ).await; assert_eq!(status, StatusCode::BAD_REQUEST); } // ═══════════════════════════════════════════════════════════════════ // Login // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn login_success() { let (app, _pool) = build_test_app().await; register(&app, "loginuser", "login@test.io", DEFAULT_PASSWORD).await; let (token, refresh, json) = login(&app, "loginuser", DEFAULT_PASSWORD).await; assert!(!token.is_empty()); assert!(!refresh.is_empty()); assert_eq!(json["account"]["username"], "loginuser"); } #[tokio::test] async fn login_wrong_password() { let (app, _pool) = build_test_app().await; register(&app, "wrongpwd", "wrong@test.io", DEFAULT_PASSWORD).await; let (status, body) = send( &app, post_public( "/api/v1/auth/login", serde_json::json!({ "username": "wrongpwd", "password": "incorrect_password" }), ), ).await; assert_eq!(status, StatusCode::UNAUTHORIZED); assert_eq!(body["error"], "AUTH_ERROR"); } #[tokio::test] async fn login_nonexistent_user() { let (app, _pool) = build_test_app().await; let (status, _) = send( &app, post_public( "/api/v1/auth/login", serde_json::json!({ "username": "ghost", "password": "whatever" }), ), ).await; assert_eq!(status, StatusCode::UNAUTHORIZED); } // ═══════════════════════════════════════════════════════════════════ // Auth chain: register → login → me (P0) // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn auth_chain_register_login_me() { let (app, _pool) = build_test_app().await; // 1. Register let (token, _refresh, json) = register(&app, "chainuser", "chain@test.io", DEFAULT_PASSWORD).await; assert_eq!(json["account"]["username"], "chainuser"); // 2. GET /me with the registration token let (status, me) = send(&app, get("/api/v1/auth/me", &token)).await; assert_eq!(status, StatusCode::OK); assert_eq!(me["username"], "chainuser"); assert_eq!(me["role"], "user"); // 3. Login separately let (token2, _, _) = login(&app, "chainuser", DEFAULT_PASSWORD).await; // 4. GET /me with the login token let (status, me2) = send(&app, get("/api/v1/auth/me", &token2)).await; assert_eq!(status, StatusCode::OK); assert_eq!(me2["id"], me["id"]); // same account } #[tokio::test] async fn me_without_token_is_unauthorized() { let (app, _pool) = build_test_app().await; let req = axum::http::Request::builder() .method("GET") .uri("/api/v1/auth/me") .body(axum::body::Body::empty()) .unwrap(); let (status, _) = send(&app, req).await; assert_eq!(status, StatusCode::UNAUTHORIZED); } #[tokio::test] async fn me_with_invalid_token_is_unauthorized() { let (app, _pool) = build_test_app().await; let (status, _) = send(&app, get("/api/v1/auth/me", "invalid.jwt.token")).await; assert_eq!(status, StatusCode::UNAUTHORIZED); } // ═══════════════════════════════════════════════════════════════════ // Refresh token // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn refresh_token_success() { let (app, _pool) = build_test_app().await; let (_, refresh, _) = register(&app, "refreshuser", "refresh@test.io", DEFAULT_PASSWORD).await; // Use refresh token to get a new pair let (status, body) = send( &app, post_public( "/api/v1/auth/refresh", serde_json::json!({ "refresh_token": refresh }), ), ).await; assert_eq!(status, StatusCode::OK); assert!(body["token"].is_string()); assert!(body["refresh_token"].is_string()); let new_token = body["token"].as_str().unwrap(); let new_refresh = body["refresh_token"].as_str().unwrap(); assert!(!new_token.is_empty()); assert!(!new_refresh.is_empty()); // New token works for /me let (status, _) = send(&app, get("/api/v1/auth/me", new_token)).await; assert_eq!(status, StatusCode::OK); } #[tokio::test] async fn refresh_token_one_time_use() { let (app, _pool) = build_test_app().await; let (_, refresh, _) = register(&app, "onetime", "onetime@test.io", DEFAULT_PASSWORD).await; // First refresh succeeds let (status, _) = send( &app, post_public( "/api/v1/auth/refresh", serde_json::json!({ "refresh_token": refresh }), ), ).await; assert_eq!(status, StatusCode::OK); // Second use of the same refresh token fails let (status, body) = send( &app, post_public( "/api/v1/auth/refresh", serde_json::json!({ "refresh_token": refresh }), ), ).await; assert_eq!(status, StatusCode::UNAUTHORIZED); } #[tokio::test] async fn refresh_with_invalid_token() { let (app, _pool) = build_test_app().await; let (status, _) = send( &app, post_public( "/api/v1/auth/refresh", serde_json::json!({ "refresh_token": "garbage" }), ), ).await; assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); } // ═══════════════════════════════════════════════════════════════════ // Password change // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn change_password_success() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "pwduser").await; let (status, _) = send( &app, put( "/api/v1/auth/password", &token, serde_json::json!({ "old_password": DEFAULT_PASSWORD, "new_password": "BrandNewP@ss1" }), ), ).await; assert_eq!(status, StatusCode::OK); // Login with new password works let (_, _, _) = login(&app, "pwduser", "BrandNewP@ss1").await; // Login with old password fails let (status, _) = send( &app, post_public( "/api/v1/auth/login", serde_json::json!({ "username": "pwduser", "password": DEFAULT_PASSWORD }), ), ).await; assert_eq!(status, StatusCode::UNAUTHORIZED); } #[tokio::test] async fn change_password_wrong_old() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "wrongold").await; let (status, _) = send( &app, put( "/api/v1/auth/password", &token, serde_json::json!({ "old_password": "wrong_old_pass", "new_password": "BrandNewP@ss1" }), ), ).await; assert_eq!(status, StatusCode::UNAUTHORIZED); } #[tokio::test] async fn change_password_too_short() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "shortpwd").await; let (status, _) = send( &app, put( "/api/v1/auth/password", &token, serde_json::json!({ "old_password": DEFAULT_PASSWORD, "new_password": "abc" }), ), ).await; assert_eq!(status, StatusCode::BAD_REQUEST); } // ═══════════════════════════════════════════════════════════════════ // TOTP 2FA (P0 chain test) // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn totp_setup_and_disable() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "totpuser").await; // Setup TOTP → returns otpauth_uri + secret let (status, body) = send(&app, post("/api/v1/auth/totp/setup", &token, serde_json::json!({}))).await; assert_eq!(status, StatusCode::OK); assert!(body["otpauth_uri"].is_string()); assert!(body["secret"].is_string()); // Disable TOTP (requires password) let (status, body) = send( &app, post("/api/v1/auth/totp/disable", &token, serde_json::json!({ "password": DEFAULT_PASSWORD })), ).await; assert_eq!(status, StatusCode::OK); // After disable, login without TOTP code succeeds let (_, _, login_json) = login(&app, "totpuser", DEFAULT_PASSWORD).await; assert_eq!(login_json["account"]["totp_enabled"], false); } #[tokio::test] async fn totp_disable_wrong_password() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "totpwrong").await; // Setup first send(&app, post("/api/v1/auth/totp/setup", &token, serde_json::json!({}))).await; // Try disable with wrong password let (status, _) = send( &app, post( "/api/v1/auth/totp/disable", &token, serde_json::json!({ "password": "wrong_password_here" }), ), ).await; assert_eq!(status, StatusCode::UNAUTHORIZED); } #[tokio::test] async fn totp_verify_wrong_code() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "totpbadcode").await; // Setup send(&app, post("/api/v1/auth/totp/setup", &token, serde_json::json!({}))).await; // Verify with a definitely-wrong code let (status, _) = send( &app, post( "/api/v1/auth/totp/verify", &token, serde_json::json!({ "code": "000000" }), ), ).await; assert_eq!(status, StatusCode::BAD_REQUEST); } // ═══════════════════════════════════════════════════════════════════ // Health endpoint // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn health_check() { let (app, _pool) = build_test_app().await; let req = axum::http::Request::builder() .uri("/api/health") .body(axum::body::Body::empty()) .unwrap(); let (status, _) = send(&app, req).await; assert_eq!(status, StatusCode::OK); }