feat(saas): Phase 2 — 模型配置模块
- Provider CRUD (列表/详情/创建/更新/删除) - Model CRUD (列表/详情/创建/更新/删除) - Account API Key 管理 (创建/轮换/撤销/掩码显示) - Usage 统计 (总量/按模型/按天, 支持时间/供应商/模型过滤) - 权限控制 (provider:manage, model:manage) - 3 个新集成测试覆盖 providers/models/keys
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! Phase 1 集成测试
|
||||
//! 集成测试 (Phase 1 + Phase 2)
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -21,6 +21,7 @@ async fn build_test_app() -> axum::Router {
|
||||
|
||||
let protected_routes = zclaw_saas::auth::protected_routes()
|
||||
.merge(zclaw_saas::account::routes())
|
||||
.merge(zclaw_saas::model_config::routes())
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
zclaw_saas::auth::auth_middleware,
|
||||
@@ -32,43 +33,45 @@ async fn build_test_app() -> axum::Router {
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_and_login() {
|
||||
let app = build_test_app().await;
|
||||
|
||||
// 注册
|
||||
let req = Request::builder()
|
||||
/// 注册并登录,返回 JWT token
|
||||
async fn register_and_login(app: &axum::Router, username: &str, email: &str) -> String {
|
||||
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": "testuser",
|
||||
"email": "test@example.com",
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": "password123"
|
||||
})).unwrap()))
|
||||
.unwrap();
|
||||
app.clone().oneshot(register_req).await.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
|
||||
// 登录
|
||||
let req = Request::builder()
|
||||
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": "testuser",
|
||||
"username": username,
|
||||
"password": "password123"
|
||||
})).unwrap()))
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
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();
|
||||
assert!(body.get("token").is_some());
|
||||
assert_eq!(body["account"]["username"], "testuser");
|
||||
body["token"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn auth_header(token: &str) -> String {
|
||||
format!("Bearer {}", token)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_register_and_login() {
|
||||
let app = build_test_app().await;
|
||||
let token = register_and_login(&app, "testuser", "test@example.com").await;
|
||||
assert!(!token.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -119,21 +122,8 @@ async fn test_unauthorized_access() {
|
||||
#[tokio::test]
|
||||
async fn test_login_wrong_password() {
|
||||
let app = build_test_app().await;
|
||||
register_and_login(&app, "wrongpwd", "wrongpwd@example.com").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")
|
||||
@@ -151,41 +141,14 @@ async fn test_login_wrong_password() {
|
||||
#[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();
|
||||
let token = register_and_login(&app, "fulltest", "full@example.com").await;
|
||||
|
||||
// 创建 API Token
|
||||
let create_token_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/tokens")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::from(serde_json::to_string(&json!({
|
||||
"name": "test-token",
|
||||
"permissions": ["model:read", "relay:use"]
|
||||
@@ -196,13 +159,13 @@ async fn test_full_authenticated_flow() {
|
||||
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 仅创建时返回
|
||||
assert!(!body["token"].is_null());
|
||||
|
||||
// 列出 Tokens
|
||||
let list_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/tokens")
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
@@ -213,10 +176,115 @@ async fn test_full_authenticated_flow() {
|
||||
let logs_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/logs/operations")
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let resp = app.oneshot(logs_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
// ============ Phase 2: 模型配置测试 ============
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_providers_crud() {
|
||||
let app = build_test_app().await;
|
||||
// 注册 super_admin 角色用户 (通过直接插入角色权限)
|
||||
let token = register_and_login(&app, "adminprov", "adminprov@example.com").await;
|
||||
|
||||
// 创建 provider (普通用户无权限 → 403)
|
||||
let create_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/providers")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::from(serde_json::to_string(&json!({
|
||||
"name": "test-provider",
|
||||
"display_name": "Test Provider",
|
||||
"base_url": "https://api.example.com/v1"
|
||||
})).unwrap()))
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(create_req).await.unwrap();
|
||||
// user 角色默认无 provider:manage 权限 → 403
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
// 列出 providers (只读权限 → 200)
|
||||
let list_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/providers")
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(list_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_models_list_and_usage() {
|
||||
let app = build_test_app().await;
|
||||
let token = register_and_login(&app, "modeluser", "modeluser@example.com").await;
|
||||
|
||||
// 列出模型 (空列表)
|
||||
let list_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/models")
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(list_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.is_array());
|
||||
assert_eq!(body.as_array().unwrap().len(), 0);
|
||||
|
||||
// 查看用量统计
|
||||
let usage_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/usage")
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(usage_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_eq!(body["total_requests"], 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_api_keys_lifecycle() {
|
||||
let app = build_test_app().await;
|
||||
let token = register_and_login(&app, "keyuser", "keyuser@example.com").await;
|
||||
|
||||
// 列出 keys (空)
|
||||
let list_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/keys")
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let resp = app.clone().oneshot(list_req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// 创建 key (需要已有 provider → 404 或由 service 层验证)
|
||||
let create_req = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/keys")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", auth_header(&token))
|
||||
.body(Body::from(serde_json::to_string(&json!({
|
||||
"provider_id": "nonexistent",
|
||||
"key_value": "sk-test-12345",
|
||||
"key_label": "Test Key"
|
||||
})).unwrap()))
|
||||
.unwrap();
|
||||
|
||||
let resp = app.clone().oneshot(create_req).await.unwrap();
|
||||
// provider 不存在 → 404
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user