//! 集成测试 (Phase 1 + Phase 2) 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}; use axum::extract::ConnectInfo; use std::net::SocketAddr; // 测试环境设置开发模式 (允许 http、默认 JWT secret) std::env::set_var("ZCLAW_SAAS_DEV", "true"); std::env::set_var("ZCLAW_SAAS_JWT_SECRET", "test-secret-for-integration-tests-only"); let db = init_memory_db().await.unwrap(); let mut config = SaaSConfig::default(); config.auth.jwt_expiration_hours = 24; let state = AppState::new(db, config).expect("测试环境 AppState 初始化失败"); let public_routes = zclaw_saas::auth::routes(); let protected_routes = zclaw_saas::auth::protected_routes() .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, )); axum::Router::new() .merge(public_routes) .merge(protected_routes) .with_state(state) .layer(axum::middleware::from_fn( |mut req: axum::extract::Request, next: axum::middleware::Next| async move { req.extensions_mut().insert(ConnectInfo::( "127.0.0.1:0".parse().unwrap(), )); next.run(req).await }, )) } /// 注册并登录,返回 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": username, "email": email, "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": username, "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(); 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] 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; register_and_login(&app, "wrongpwd", "wrongpwd@example.com").await; 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 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", auth_header(&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()); // 列出 Tokens let list_req = Request::builder() .method("GET") .uri("/api/v1/tokens") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); let resp = app.clone().oneshot(list_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 查看操作日志 (普通用户无 admin 权限 → 403) let logs_req = Request::builder() .method("GET") .uri("/api/v1/logs/operations") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); let resp = app.oneshot(logs_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); } // ============ 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); } // ============ Phase 3: 中转服务测试 ============ #[tokio::test] async fn test_relay_models_list() { let app = build_test_app().await; let token = register_and_login(&app, "relayuser", "relayuser@example.com").await; // 列出可用中转模型 (空列表,因为没有 provider/model 种子数据) let req = Request::builder() .method("GET") .uri("/api/v1/relay/models") .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!(body.is_array()); } #[tokio::test] async fn test_relay_chat_no_model() { let app = build_test_app().await; let token = register_and_login(&app, "relayfail", "relayfail@example.com").await; // 尝试中转到不存在的模型 let req = Request::builder() .method("POST") .uri("/api/v1/relay/chat/completions") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "model": "nonexistent-model", "messages": [{"role": "user", "content": "hello"}] })).unwrap())) .unwrap(); let resp = app.clone().oneshot(req).await.unwrap(); // 模型不存在 → 404 assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_relay_tasks_list() { let app = build_test_app().await; let token = register_and_login(&app, "relaytasks", "relaytasks@example.com").await; let req = Request::builder() .method("GET") .uri("/api/v1/relay/tasks") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); 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); } // ============ Phase 4: 设备注册 + 密码修改 ============ #[tokio::test] async fn test_device_register_and_list() { let app = build_test_app().await; let token = register_and_login(&app, "devuser", "devuser@example.com").await; // 注册设备 let reg_req = Request::builder() .method("POST") .uri("/api/v1/devices/register") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_id": "desktop-test-001", "device_name": "My Desktop", "platform": "windows", "app_version": "0.1.0" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(reg_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 列出设备 — 应该有 1 台 let list_req = Request::builder() .method("GET") .uri("/api/v1/devices") .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(), 1); assert_eq!(body[0]["device_id"], "desktop-test-001"); assert_eq!(body[0]["platform"], "windows"); } #[tokio::test] async fn test_device_upsert_on_reregister() { let app = build_test_app().await; let token = register_and_login(&app, "upsertdev", "upsertdev@example.com").await; // 第一次注册 let req = Request::builder() .method("POST") .uri("/api/v1/devices/register") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_id": "device-upsert-01", "device_name": "Old Name", "platform": "linux" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 同一 device_id 再次注册 (UPSERT → 更新 device_name 和 last_seen_at) let req2 = Request::builder() .method("POST") .uri("/api/v1/devices/register") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_id": "device-upsert-01", "device_name": "New Name", "platform": "linux" })).unwrap())) .unwrap(); let resp2 = app.clone().oneshot(req2).await.unwrap(); assert_eq!(resp2.status(), StatusCode::OK); // 仍然只有 1 台设备,名称已更新 let list_req = Request::builder() .method("GET") .uri("/api/v1/devices") .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_eq!(body.as_array().unwrap().len(), 1); assert_eq!(body[0]["device_name"], "New Name"); } #[tokio::test] async fn test_device_heartbeat() { let app = build_test_app().await; let token = register_and_login(&app, "hbuser", "hbuser@example.com").await; // 先注册设备 let reg_req = Request::builder() .method("POST") .uri("/api/v1/devices/register") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_id": "hb-device-01", "device_name": "Heartbeat Device" })).unwrap())) .unwrap(); app.clone().oneshot(reg_req).await.unwrap(); // 发送心跳 let hb_req = Request::builder() .method("POST") .uri("/api/v1/devices/heartbeat") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_id": "hb-device-01" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(hb_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 心跳到未注册的设备 → 404 let bad_hb = Request::builder() .method("POST") .uri("/api/v1/devices/heartbeat") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_id": "nonexistent-device" })).unwrap())) .unwrap(); let resp = app.oneshot(bad_hb).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_device_register_missing_id() { let app = build_test_app().await; let token = register_and_login(&app, "baddev", "baddev@example.com").await; // 缺少 device_id → 400 let req = Request::builder() .method("POST") .uri("/api/v1/devices/register") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_name": "No ID Device" })).unwrap())) .unwrap(); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_change_password() { let app = build_test_app().await; let token = register_and_login(&app, "pwduser", "pwduser@example.com").await; // 修改密码 let change_req = Request::builder() .method("PUT") .uri("/api/v1/auth/password") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "old_password": "password123", "new_password": "newpassword456" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(change_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 用新密码重新登录 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": "pwduser", "password": "newpassword456" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(login_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 用旧密码登录 → 401 let old_login = Request::builder() .method("POST") .uri("/api/v1/auth/login") .header("Content-Type", "application/json") .body(Body::from(serde_json::to_string(&json!({ "username": "pwduser", "password": "password123" })).unwrap())) .unwrap(); let resp = app.oneshot(old_login).await.unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_change_password_wrong_old() { let app = build_test_app().await; let token = register_and_login(&app, "wrongold", "wrongold@example.com").await; // 旧密码错误 → 400 let change_req = Request::builder() .method("PUT") .uri("/api/v1/auth/password") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "old_password": "wrong-old-password", "new_password": "newpassword456" })).unwrap())) .unwrap(); let resp = app.oneshot(change_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } // ============ Phase 4 E2E: 完整生命周期 ============ #[tokio::test] async fn test_e2e_full_lifecycle() { let app = build_test_app().await; // 1. 注册 let token = register_and_login(&app, "e2euser", "e2e@example.com").await; assert!(!token.is_empty()); // 2. 查看自己的账号信息 let me_req = Request::builder() .method("GET") .uri("/api/v1/auth/me") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); let resp = app.clone().oneshot(me_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 me: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap(); assert_eq!(me["username"], "e2euser"); // 3. 注册设备 let dev_req = Request::builder() .method("POST") .uri("/api/v1/devices/register") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "device_id": "e2e-device-001", "device_name": "E2E Test Machine", "platform": "windows", "app_version": "1.0.0" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(dev_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 4. 创建 API Token let tok_req = Request::builder() .method("POST") .uri("/api/v1/tokens") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "name": "e2e-token", "permissions": ["model:read"] })).unwrap())) .unwrap(); let resp = app.clone().oneshot(tok_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 tok_body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap(); assert!(!tok_body["token"].is_null()); // 5. 修改密码 let pwd_req = Request::builder() .method("PUT") .uri("/api/v1/auth/password") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "old_password": "password123", "new_password": "e2e-new-password" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(pwd_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 6. 用新密码登录验证 let relogin_req = Request::builder() .method("POST") .uri("/api/v1/auth/login") .header("Content-Type", "application/json") .body(Body::from(serde_json::to_string(&json!({ "username": "e2euser", "password": "e2e-new-password" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(relogin_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 login_body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap(); let new_token = login_body["token"].as_str().unwrap(); assert!(!new_token.is_empty()); // 7. 用新 token 列出设备 let list_req = Request::builder() .method("GET") .uri("/api/v1/devices") .header("Authorization", auth_header(new_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 devs: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap(); assert_eq!(devs.as_array().unwrap().len(), 1); // 8. 用旧 token 应该仍可用 (JWT 未撤销) let old_tok_list = Request::builder() .method("GET") .uri("/api/v1/devices") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); let resp = app.oneshot(old_tok_list).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[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); } // ============ P2: TOTP 2FA ============ #[tokio::test] async fn test_totp_setup_and_verify() { let app = build_test_app().await; let token = register_and_login(&app, "totpuser", "totp@example.com").await; // 1. Setup TOTP let setup_req = Request::builder() .method("POST") .uri("/api/v1/auth/totp/setup") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); let resp = app.clone().oneshot(setup_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["otpauth_uri"].is_string()); assert!(body["secret"].is_string()); let secret = body["secret"].as_str().unwrap(); // 2. Verify with wrong code → 400 let bad_verify = Request::builder() .method("POST") .uri("/api/v1/auth/totp/verify") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({"code": "000000"})).unwrap())) .unwrap(); let resp = app.clone().oneshot(bad_verify).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); // 3. Disable TOTP (password required) let disable_req = Request::builder() .method("POST") .uri("/api/v1/auth/totp/disable") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({"password": "password123"})).unwrap())) .unwrap(); let resp = app.clone().oneshot(disable_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // 4. TOTP disabled → login without totp_code should succeed 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": "totpuser", "password": "password123" })).unwrap())) .unwrap(); let resp = app.oneshot(login_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[tokio::test] async fn test_totp_disabled_login_without_code() { let app = build_test_app().await; let token = register_and_login(&app, "nototp", "nototp@example.com").await; // TOTP not enabled → login without totp_code is fine 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": "nototp", "password": "password123" })).unwrap())) .unwrap(); let resp = app.clone().oneshot(login_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // Setup TOTP let setup_req = Request::builder() .method("POST") .uri("/api/v1/auth/totp/setup") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); app.clone().oneshot(setup_req).await.unwrap(); // Don't verify — try login without TOTP code → should fail let login_req2 = Request::builder() .method("POST") .uri("/api/v1/auth/login") .header("Content-Type", "application/json") .body(Body::from(serde_json::to_string(&json!({ "username": "nototp", "password": "password123" })).unwrap())) .unwrap(); // Note: TOTP is set up but not yet verified/enabled, so login should still work // (totp_enabled is still 0 until verify is called) let resp = app.oneshot(login_req2).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[tokio::test] async fn test_totp_disable_wrong_password() { let app = build_test_app().await; let token = register_and_login(&app, "totpwrong", "totpwrong@example.com").await; let disable_req = Request::builder() .method("POST") .uri("/api/v1/auth/totp/disable") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({"password": "wrong"})).unwrap())) .unwrap(); let resp = app.oneshot(disable_req).await.unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } // ============ P2: 配置同步 ============ #[tokio::test] async fn test_config_diff() { let app = build_test_app().await; let token = register_and_login(&app, "diffuser", "diffuser@example.com").await; // Diff with no data let diff_req = Request::builder() .method("POST") .uri("/api/v1/config/diff") .header("Content-Type", "application/json") .header("Authorization", auth_header(&token)) .body(Body::from(serde_json::to_string(&json!({ "client_fingerprint": "test-client", "action": "push", "config_keys": ["server.host", "agent.defaults.default_model"], "client_values": {"server.host": "0.0.0.0", "agent.defaults.default_model": "test-model"} })).unwrap())) .unwrap(); let resp = app.clone().oneshot(diff_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_keys"], 2); assert!(body["items"].is_array()); } #[tokio::test] async fn test_config_sync_push() { let app = build_test_app().await; let token = register_and_login(&app, "syncpush", "syncpush@example.com").await; // Seed config (admin only → 403 for regular user, skip) // Push config 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", "action": "push", "config_keys": ["server.host", "server.port"], "client_values": {"server.host": "192.168.1.1", "server.port": "9090"} })).unwrap())) .unwrap(); let resp = app.clone().oneshot(sync_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(); // Keys don't exist in SaaS yet → all skipped assert_eq!(body["skipped"], 2); } #[tokio::test] async fn test_relay_retry_unauthorized() { let app = build_test_app().await; let token = register_and_login(&app, "retryuser", "retryuser@example.com").await; // Retry requires relay:admin → 403 for regular user let retry_req = Request::builder() .method("POST") .uri("/api/v1/relay/tasks/nonexistent/retry") .header("Authorization", auth_header(&token)) .body(Body::empty()) .unwrap(); let resp = app.oneshot(retry_req).await.unwrap(); // 404: task not found (correct behavior, 403 requires relay:admin) assert_ne!(resp.status(), StatusCode::OK); }