use std::sync::Arc; use axum::body::Body; use axum::http::{Request, StatusCode}; use tower::ServiceExt; use config_memory::MemoryConfigStore; use tcp_server::TcpEventBus; use http_api::{AppState, router}; fn test_app() -> axum::Router { let config = Arc::new(MemoryConfigStore::new()); let events = Arc::new(TcpEventBus::new(16)); let state = AppState { config, events }; router(state) } fn json_request(method: &str, uri: &str, body: Option<&str>) -> Request { let mut builder = Request::builder() .method(method) .uri(uri) .header("content-type", "application/json"); if let Some(b) = body { builder.body(Body::from(b.to_string())).unwrap() } else { builder.body(Body::empty()).unwrap() } } #[tokio::test] async fn create_and_get_widget() { let app = test_app(); let body = r#"{ "id": 1, "name": "weather", "display_hint": "icon_value", "data_source_id": 10, "mappings": [{"source_path": "$.temp", "target_key": "temperature"}] }"#; let resp = app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["name"], "weather"); assert_eq!(json["display_hint"], "icon_value"); assert_eq!(json["data_source_id"], 10); } #[tokio::test] async fn list_widgets() { let app = test_app(); let w1 = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#; let w2 = r#"{"id":2,"name":"b","display_hint":"key_value","data_source_id":2,"mappings":[]}"#; app.clone().oneshot(json_request("POST", "/api/widgets", Some(w1))).await.unwrap(); app.clone().oneshot(json_request("POST", "/api/widgets", Some(w2))).await.unwrap(); let resp = app.oneshot(json_request("GET", "/api/widgets", None)).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let json: Vec = serde_json::from_slice(&body).unwrap(); assert_eq!(json.len(), 2); } #[tokio::test] async fn delete_widget() { let app = test_app(); let body = r#"{"id":1,"name":"a","display_hint":"icon_value","data_source_id":1,"mappings":[]}"#; app.clone().oneshot(json_request("POST", "/api/widgets", Some(body))).await.unwrap(); let resp = app.clone().oneshot(json_request("DELETE", "/api/widgets/1", None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::NO_CONTENT); let resp = app.oneshot(json_request("GET", "/api/widgets/1", None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn create_and_get_data_source() { let app = test_app(); let body = r#"{ "id": 10, "name": "weather_api", "source_type": "weather", "poll_interval_secs": 300, "url": "https://api.openweather.org", "api_key": "test-key", "headers": [] }"#; let resp = app.clone().oneshot(json_request("POST", "/api/data-sources", Some(body))).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); let resp = app.oneshot(json_request("GET", "/api/data-sources/10", None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["name"], "weather_api"); assert_eq!(json["poll_interval_secs"], 300); } #[tokio::test] async fn update_and_get_layout() { let app = test_app(); let body = r#"{ "root": { "type": "container", "direction": "row", "gap": 4, "padding": 2, "children": [ {"sizing": {"type": "flex", "value": 1}, "node": {"type": "leaf", "widget_id": 1}}, {"sizing": {"type": "fixed", "value": 80}, "node": {"type": "leaf", "widget_id": 2}} ] } }"#; let resp = app.clone().oneshot(json_request("PUT", "/api/layout", Some(body))).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let resp = app.oneshot(json_request("GET", "/api/layout", None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["root"]["type"], "container"); assert_eq!(json["root"]["direction"], "row"); assert_eq!(json["root"]["children"].as_array().unwrap().len(), 2); } #[tokio::test] async fn get_nonexistent_returns_404() { let app = test_app(); let resp = app.clone().oneshot(json_request("GET", "/api/widgets/99", None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let resp = app.oneshot(json_request("GET", "/api/data-sources/99", None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); }