- 新增 zclaw-saas crate 作为 workspace 成员 - 配置系统 (TOML + 环境变量覆盖) - 错误类型体系 (SaasError 16 变体, IntoResponse) - SQLite 数据库 (12 表 schema, 内存/文件双模式, 3 系统角色种子数据) - JWT 认证 (签发/验证/刷新) - Argon2id 密码哈希 - 认证中间件 (公开/受保护路由分层) - 账号管理 CRUD + API Token 管理 + 操作日志 - 7 单元测试 + 5 集成测试全部通过
223 lines
6.9 KiB
Rust
223 lines
6.9 KiB
Rust
//! Phase 1 集成测试
|
|
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use serde_json::json;
|
|
use tower::ServiceExt;
|
|
|
|
const MAX_BODY_SIZE: usize = 1024 * 1024; // 1MB
|
|
|
|
async fn build_test_app() -> axum::Router {
|
|
use zclaw_saas::{config::SaaSConfig, db::init_memory_db, state::AppState};
|
|
|
|
let db = init_memory_db().await.unwrap();
|
|
let mut config = SaaSConfig::default();
|
|
config.auth.jwt_expiration_hours = 24;
|
|
let state = AppState::new(db, config);
|
|
|
|
let public_routes = zclaw_saas::auth::routes();
|
|
|
|
let protected_routes = zclaw_saas::auth::protected_routes()
|
|
.merge(zclaw_saas::account::routes())
|
|
.layer(axum::middleware::from_fn_with_state(
|
|
state.clone(),
|
|
zclaw_saas::auth::auth_middleware,
|
|
));
|
|
|
|
axum::Router::new()
|
|
.merge(public_routes)
|
|
.merge(protected_routes)
|
|
.with_state(state)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_register_and_login() {
|
|
let app = build_test_app().await;
|
|
|
|
// 注册
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/register")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&json!({
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"password": "password123"
|
|
})).unwrap()))
|
|
.unwrap();
|
|
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
|
|
// 登录
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&json!({
|
|
"username": "testuser",
|
|
"password": "password123"
|
|
})).unwrap()))
|
|
.unwrap();
|
|
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
|
|
let body_bytes = axum::body::to_bytes(resp.into_body(), MAX_BODY_SIZE).await.unwrap();
|
|
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
|
|
assert!(body.get("token").is_some());
|
|
assert_eq!(body["account"]["username"], "testuser");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_register_duplicate_fails() {
|
|
let app = build_test_app().await;
|
|
|
|
let body = json!({
|
|
"username": "dupuser",
|
|
"email": "dup@example.com",
|
|
"password": "password123"
|
|
});
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/register")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&body).unwrap()))
|
|
.unwrap();
|
|
|
|
let resp = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/register")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&body).unwrap()))
|
|
.unwrap();
|
|
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CONFLICT);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_unauthorized_access() {
|
|
let app = build_test_app().await;
|
|
|
|
let req = Request::builder()
|
|
.method("GET")
|
|
.uri("/api/v1/accounts")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_login_wrong_password() {
|
|
let app = build_test_app().await;
|
|
|
|
// 先注册
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/register")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&json!({
|
|
"username": "wrongpwd",
|
|
"email": "wrongpwd@example.com",
|
|
"password": "password123"
|
|
})).unwrap()))
|
|
.unwrap();
|
|
app.clone().oneshot(req).await.unwrap();
|
|
|
|
// 错误密码登录
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&json!({
|
|
"username": "wrongpwd",
|
|
"password": "wrong_password"
|
|
})).unwrap()))
|
|
.unwrap();
|
|
|
|
let resp = app.oneshot(req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_full_authenticated_flow() {
|
|
let app = build_test_app().await;
|
|
|
|
// 注册 + 登录
|
|
let register_req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/register")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&json!({
|
|
"username": "fulltest",
|
|
"email": "full@example.com",
|
|
"password": "password123"
|
|
})).unwrap()))
|
|
.unwrap();
|
|
app.clone().oneshot(register_req).await.unwrap();
|
|
|
|
let login_req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header("Content-Type", "application/json")
|
|
.body(Body::from(serde_json::to_string(&json!({
|
|
"username": "fulltest",
|
|
"password": "password123"
|
|
})).unwrap()))
|
|
.unwrap();
|
|
|
|
let resp = app.clone().oneshot(login_req).await.unwrap();
|
|
let body_bytes = axum::body::to_bytes(resp.into_body(), MAX_BODY_SIZE).await.unwrap();
|
|
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
|
|
let token = body["token"].as_str().unwrap().to_string();
|
|
|
|
// 创建 API Token
|
|
let create_token_req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/tokens")
|
|
.header("Content-Type", "application/json")
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.body(Body::from(serde_json::to_string(&json!({
|
|
"name": "test-token",
|
|
"permissions": ["model:read", "relay:use"]
|
|
})).unwrap()))
|
|
.unwrap();
|
|
|
|
let resp = app.clone().oneshot(create_token_req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let body_bytes = axum::body::to_bytes(resp.into_body(), MAX_BODY_SIZE).await.unwrap();
|
|
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
|
|
assert!(!body["token"].is_null()); // 原始 token 仅创建时返回
|
|
|
|
// 列出 Tokens
|
|
let list_req = Request::builder()
|
|
.method("GET")
|
|
.uri("/api/v1/tokens")
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
|
|
let resp = app.clone().oneshot(list_req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
|
|
// 查看操作日志
|
|
let logs_req = Request::builder()
|
|
.method("GET")
|
|
.uri("/api/v1/logs/operations")
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
|
|
let resp = app.oneshot(logs_req).await.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|