chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
385
crates/zclaw-saas/tests/auth_test.rs
Normal file
385
crates/zclaw-saas/tests/auth_test.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user