Files
zclaw_openfang/crates/zclaw-saas/tests/auth_test.rs
iven 5fdf96c3f5 chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
2026-03-29 10:46:41 +08:00

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);
}