feat(saas): Phase 4 — 配置迁移模块

- 配置项 CRUD (列表/详情/创建/更新/删除)
- 配置分析端点 (按类别汇总, SaaS 托管统计)
- 13 个默认配置项种子数据 (server/agent/memory/llm)
- 配置同步协议 (客户端→SaaS, SaaS 优先策略)
- 同步日志记录和查询
- 3 个新集成测试覆盖配置迁移端点
This commit is contained in:
iven
2026-03-27 12:52:42 +08:00
parent a99a3df9dd
commit 00a08c9f9b
6 changed files with 569 additions and 0 deletions

View File

@@ -23,6 +23,7 @@ async fn build_test_app() -> axum::Router {
.merge(zclaw_saas::account::routes())
.merge(zclaw_saas::model_config::routes())
.merge(zclaw_saas::relay::routes())
.merge(zclaw_saas::migration::routes())
.layer(axum::middleware::from_fn_with_state(
state.clone(),
zclaw_saas::auth::auth_middleware,
@@ -349,3 +350,92 @@ async fn test_relay_tasks_list() {
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
// ============ Phase 4: 配置迁移测试 ============
#[tokio::test]
async fn test_config_analysis_empty() {
let app = build_test_app().await;
let token = register_and_login(&app, "cfguser", "cfguser@example.com").await;
// 初始分析 (无种子数据 → 空列表)
let req = Request::builder()
.method("GET")
.uri("/api/v1/config/analysis")
.header("Authorization", auth_header(&token))
.body(Body::empty())
.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_eq!(body["total_items"], 0);
}
#[tokio::test]
async fn test_config_seed_and_list() {
let app = build_test_app().await;
let token = register_and_login(&app, "cfgseed", "cfgseed@example.com").await;
// 种子配置 (普通用户无权限 → 403)
let seed_req = Request::builder()
.method("POST")
.uri("/api/v1/config/seed")
.header("Authorization", auth_header(&token))
.body(Body::empty())
.unwrap();
let resp = app.clone().oneshot(seed_req).await.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
// 列出配置项 (空列表)
let list_req = Request::builder()
.method("GET")
.uri("/api/v1/config/items")
.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);
}
#[tokio::test]
async fn test_config_sync() {
let app = build_test_app().await;
let token = register_and_login(&app, "cfgsync", "cfgsync@example.com").await;
let sync_req = Request::builder()
.method("POST")
.uri("/api/v1/config/sync")
.header("Content-Type", "application/json")
.header("Authorization", auth_header(&token))
.body(Body::from(serde_json::to_string(&json!({
"client_fingerprint": "test-desktop-v1",
"config_keys": ["server.host", "agent.defaults.default_model"],
"client_values": {
"server.host": "0.0.0.0",
"agent.defaults.default_model": "deepseek/deepseek-chat"
}
})).unwrap()))
.unwrap();
let resp = app.clone().oneshot(sync_req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
// 查看同步日志
let logs_req = Request::builder()
.method("GET")
.uri("/api/v1/config/sync-logs")
.header("Authorization", auth_header(&token))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(logs_req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}