386 lines
14 KiB
Rust
386 lines
14 KiB
Rust
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);
|
|
}
|