use axum::{ body::Body, http::{Method, Request, StatusCode, header}, }; use http_body_util::BodyExt; use serde_json::{Value, json}; use tempfile::TempDir; use tower::ServiceExt; async fn test_app() -> (axum::Router, TempDir) { let temp_dir = TempDir::new().expect("temporary directory should be created"); let pool = task_pilot_backend::database::connect("sqlite::memory:") .await .expect("test database should initialize"); let app = task_pilot_backend::app(pool, temp_dir.path().join("logs")) .await .expect("test app should start"); (app, temp_dir) } async fn auth_token(app: &axum::Router) -> String { let response = app .clone() .oneshot( Request::builder() .uri("/api/v1/session") .body(Body::empty()) .expect("request should build"), ) .await .expect("router should respond"); let body = response .into_body() .collect() .await .expect("body should be readable") .to_bytes(); let json: Value = serde_json::from_slice(&body).expect("session should be JSON"); json["token"] .as_str() .expect("token should be a string") .to_owned() } async fn send_json( app: &axum::Router, method: Method, uri: &str, token: Option<&str>, body: Option, ) -> (StatusCode, Value) { let mut builder = Request::builder().method(method).uri(uri); if let Some(token) = token { builder = builder.header("x-taskpilot-token", token); } let request = if let Some(body) = body { builder .header(header::CONTENT_TYPE, "application/json") .body(Body::from( serde_json::to_vec(&body).expect("body should serialize"), )) .expect("request should build") } else { builder.body(Body::empty()).expect("request should build") }; let response = app .clone() .oneshot(request) .await .expect("router should respond"); let status = response.status(); let body = response .into_body() .collect() .await .expect("body should be readable") .to_bytes(); let json = serde_json::from_slice(&body).unwrap_or(Value::Null); (status, json) } #[tokio::test] async fn seeds_python_mirrors_with_one_default() { let (app, _temp_dir) = test_app().await; let (status, body) = send_json(&app, Method::GET, "/api/v1/python/mirrors", None, None).await; assert_eq!(status, StatusCode::OK); let mirrors = body.as_array().expect("mirrors should be an array"); assert_eq!(mirrors.len(), 4); let defaults: Vec<&Value> = mirrors .iter() .filter(|mirror| mirror["is_default"] == true) .collect(); assert_eq!(defaults.len(), 1); assert_eq!(defaults[0]["id"], "tsinghua"); assert!(mirrors.iter().all(|mirror| mirror["enabled"] == true)); } #[tokio::test] async fn mirrors_crud_and_default_switch() { let (app, _temp_dir) = test_app().await; let token = auth_token(&app).await; let (status, created) = send_json( &app, Method::POST, "/api/v1/python/mirrors", Some(&token), Some(json!({ "name": "内网镜像", "index_url": "https://pypi.example.local/simple", "trusted_host": "pypi.example.local", "enabled": true })), ) .await; assert_eq!(status, StatusCode::CREATED); let id = created["id"] .as_str() .expect("created mirror should have an id") .to_owned(); assert_eq!(created["is_default"], false); assert_eq!(created["trusted_host"], "pypi.example.local"); let (status, mirror) = send_json( &app, Method::POST, &format!("/api/v1/python/mirrors/{id}/default"), Some(&token), None, ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(mirror["is_default"], true); let (status, list) = send_json(&app, Method::GET, "/api/v1/python/mirrors", None, None).await; assert_eq!(status, StatusCode::OK); let defaults = list .as_array() .expect("mirrors should be an array") .iter() .filter(|mirror| mirror["is_default"] == true) .count(); assert_eq!(defaults, 1); let (status, updated) = send_json( &app, Method::PUT, &format!("/api/v1/python/mirrors/{id}"), Some(&token), Some(json!({ "name": "内网镜像(停用)", "index_url": "https://pypi.example.local/simple", "trusted_host": null, "enabled": false })), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(updated["name"], "内网镜像(停用)"); assert_eq!(updated["enabled"], false); // 停用默认镜像时连带清掉默认标记,避免执行器继续引用一个停用的源 assert_eq!(updated["is_default"], false); let (status, _) = send_json( &app, Method::DELETE, &format!("/api/v1/python/mirrors/{id}"), Some(&token), None, ) .await; assert_eq!(status, StatusCode::NO_CONTENT); let (status, body) = send_json( &app, Method::GET, &format!("/api/v1/python/mirrors/{id}"), None, None, ) .await; assert_eq!(status, StatusCode::NOT_FOUND); assert_eq!(body["code"], "MIRROR_NOT_FOUND"); } #[tokio::test] async fn rejects_invalid_mirror_url_and_duplicate_index() { let (app, _temp_dir) = test_app().await; let token = auth_token(&app).await; let (status, body) = send_json( &app, Method::POST, "/api/v1/python/mirrors", Some(&token), Some(json!({ "name": "坏地址", "index_url": "ftp://example.com/simple", "enabled": true })), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(body["field"], "index_url"); let (status, body) = send_json( &app, Method::POST, "/api/v1/python/mirrors", Some(&token), Some(json!({ "name": "重复镜像", "index_url": "https://pypi.org/simple", "enabled": true })), ) .await; assert_eq!(status, StatusCode::CONFLICT); assert_eq!(body["code"], "MIRROR_CONFLICT"); } #[tokio::test] async fn mirror_mutations_require_token() { let (app, _temp_dir) = test_app().await; let (status, body) = send_json( &app, Method::POST, "/api/v1/python/mirrors", None, Some(json!({ "name": "未授权", "index_url": "https://example.com/simple", "enabled": true })), ) .await; assert_eq!(status, StatusCode::FORBIDDEN); assert_eq!(body["code"], "FORBIDDEN"); }