use std::fs; use axum::{ body::Body, http::{Request, StatusCode}, }; use http_body_util::BodyExt; use serde_json::Value; 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) } #[tokio::test] async fn health_endpoint_returns_service_metadata() { let (app, _temp_dir) = test_app().await; let response = app .oneshot( Request::builder() .uri("/api/v1/health") .body(Body::empty()) .expect("request should build"), ) .await .expect("router should respond"); assert_eq!(response.status(), StatusCode::OK); let body = response .into_body() .collect() .await .expect("response body should be readable") .to_bytes(); let json: Value = serde_json::from_slice(&body).expect("response should be JSON"); assert_eq!(json["status"], "ok"); assert_eq!(json["service"], "task-pilot-backend"); assert_eq!(json["version"], env!("CARGO_PKG_VERSION")); } #[tokio::test] async fn openapi_endpoint_describes_health_api() { let (app, _temp_dir) = test_app().await; let response = app .oneshot( Request::builder() .uri("/api/v1/openapi.json") .body(Body::empty()) .expect("request should build"), ) .await .expect("router should respond"); assert_eq!(response.status(), StatusCode::OK); let body = response .into_body() .collect() .await .expect("response body should be readable") .to_bytes(); let json: Value = serde_json::from_slice(&body).expect("response should be JSON"); assert_eq!(json["info"]["title"], "TaskPilot API"); assert!(json["paths"]["/api/v1/health"]["get"].is_object()); assert!(json["paths"]["/api/v1/system/directories"]["get"].is_object()); } fn encode_query_path(path: &std::path::Path) -> String { path.to_string_lossy() .replace('%', "%25") .replace(':', "%3A") .replace('\\', "%5C") } async fn get_json(app: &axum::Router, uri: &str) -> (StatusCode, Value) { let response = app .clone() .oneshot( Request::builder() .uri(uri) .body(Body::empty()) .expect("request should build"), ) .await .expect("router should respond"); let status = response.status(); let body = response .into_body() .collect() .await .expect("response body should be readable") .to_bytes(); (status, serde_json::from_slice(&body).unwrap_or(Value::Null)) } #[tokio::test] async fn directory_listing_endpoint_returns_subdirectories() { let (app, temp_dir) = test_app().await; let project = temp_dir.path().join("project"); fs::create_dir_all(project.join("nested")).expect("nested directory should be created"); fs::write(project.join("file.txt"), "x").expect("file should be written"); let uri = format!( "/api/v1/system/directories?path={}", encode_query_path(&project) ); let (status, body) = get_json(&app, &uri).await; assert_eq!(status, StatusCode::OK); assert_eq!(body["path"], project.to_string_lossy().into_owned()); let entries = body["entries"] .as_array() .expect("entries should be an array"); assert_eq!(entries.len(), 1); assert_eq!(entries[0]["name"], "nested"); assert_eq!( entries[0]["path"], project.join("nested").to_string_lossy().into_owned() ); } #[tokio::test] async fn directory_listing_endpoint_rejects_relative_paths() { let (app, _temp_dir) = test_app().await; let (status, body) = get_json(&app, "/api/v1/system/directories?path=relative").await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(body["code"], "DIRECTORY_PATH_INVALID"); } #[tokio::test] async fn directory_listing_endpoint_returns_drives_without_path() { let (app, _temp_dir) = test_app().await; let (status, body) = get_json(&app, "/api/v1/system/directories").await; assert_eq!(status, StatusCode::OK); assert!(body["path"].is_null()); assert!( !body["entries"] .as_array() .expect("entries should be an array") .is_empty() ); }