//! 集成测试 (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}; 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()) .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) } /// 注册并登录,返回 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); // 查看操作日志 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::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); } // ============ 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); } #[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); }