use axum::{ body::Body, http::{Request, StatusCode}, }; use openfut_bridge::{ capture::CapturedRequest, config::Config, mapper::{map_to_core, placeholder_response}, proxy::ProxyState, routes, }; use axum::routing::{any, delete, get, post}; use tower::ServiceExt; // ── Unit tests ──────────────────────────────────────────────────────────────── #[test] fn test_known_endpoint_maps_to_core() { let mapping = map_to_core("POST", "/ut/auth"); assert!(mapping.is_some()); let m = mapping.unwrap(); assert_eq!(m.core_path, "/auth/local"); } #[test] fn test_unknown_endpoint_returns_none() { let mapping = map_to_core("GET", "/ut/game/fut/some/unknown/path"); assert!(mapping.is_none()); } #[test] fn test_capture_serializes_cleanly() { let capture = CapturedRequest::new( "GET", "/ut/game/fut/user/settings", None, vec![("user-agent".into(), "FIFA23/1.0".into())], None, ); let json = serde_json::to_string(&capture).expect("serialize"); let back: CapturedRequest = serde_json::from_str(&json).expect("deserialize"); assert_eq!(back.path, "/ut/game/fut/user/settings"); assert_eq!(back.method, "GET"); assert!(back.mapped_to_core.is_none()); assert!(back.response_status.is_none()); } #[test] fn test_capture_with_response() { let capture = CapturedRequest::new( "POST", "/ut/auth", None, vec![], Some(r#"{"token":"abc"}"#.into()), ) .with_response(200, Some(r#"{"status":"ok"}"#.into())); assert_eq!(capture.response_status, Some(200)); assert!(capture.response_body.is_some()); } // ── #24 Placeholder response format ────────────────────────────────────────── #[test] fn test_placeholder_response_has_required_fields() { let resp = placeholder_response("GET", "/ut/game/fut/unknown"); assert_eq!(resp["status"], "ok"); assert!(resp["openfut_note"].is_string()); assert_eq!(resp["method"], "GET"); assert_eq!(resp["path"], "/ut/game/fut/unknown"); } #[test] fn test_placeholder_response_for_post() { let resp = placeholder_response("POST", "/ut/auth/fifa"); assert_eq!(resp["status"], "ok"); assert_eq!(resp["method"], "POST"); } // ── #25 Full HTTP integration test against bridge ──────────────────────────── fn build_test_app() -> axum::Router { let cfg = Config { listen_addr: "127.0.0.1:0".into(), core_url: "http://127.0.0.1:9999".into(), // won't be reached in placeholder mode captures_dir: "/tmp/openfut-test-captures".into(), placeholder_mode: true, tls_enabled: false, }; let state = ProxyState::new(cfg); axum::Router::new() .route("/_bridge/health", get(routes::health::get_health)) .route("/_bridge/dashboard", get(routes::health::get_dashboard)) .route("/_bridge/captures", get(routes::admin::get_captures)) .route("/_bridge/captures", delete(routes::admin::delete_captures)) .route("/_bridge/unknown", get(routes::admin::get_unknown_endpoints)) .route("/_bridge/status", get(routes::admin::get_endpoint_status)) .route("/_bridge/captures/:id/replay", post(routes::admin::post_replay_capture)) .fallback(any(openfut_bridge::proxy::catch_all_handler)) .with_state(state) } #[tokio::test] async fn test_bridge_health_endpoint() { let app = build_test_app(); let resp = app .oneshot(Request::builder().uri("/_bridge/health").body(Body::empty()).unwrap()) .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["status"], "ok"); assert_eq!(json["service"], "openfut-bridge"); } #[tokio::test] async fn test_bridge_placeholder_mode_returns_ok() { let app = build_test_app(); let resp = app .oneshot( Request::builder() .method("GET") .uri("/ut/game/fut/completely/unknown/endpoint") .body(Body::empty()) .unwrap(), ) .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["status"], "ok"); assert!(json["openfut_note"].is_string()); } #[tokio::test] async fn test_bridge_captures_endpoint_returns_list() { let app = build_test_app(); let resp = app .oneshot( Request::builder() .uri("/_bridge/captures") .body(Body::empty()) .unwrap(), ) .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!(json["captures"].is_array()); assert!(json["total"].is_number()); } #[tokio::test] async fn test_bridge_delete_captures() { let app = build_test_app(); let resp = app .oneshot( Request::builder() .method("DELETE") .uri("/_bridge/captures") .body(Body::empty()) .unwrap(), ) .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!(json["deleted"].is_number()); } #[tokio::test] async fn test_bridge_status_endpoint() { let app = build_test_app(); let resp = app .oneshot( Request::builder() .uri("/_bridge/status") .body(Body::empty()) .unwrap(), ) .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!(json["endpoints"].is_array()); } // ── Phase 13 — Dashboard ────────────────────────────────────────────────────── #[tokio::test] async fn test_dashboard_returns_html() { let app = build_test_app(); let resp = app .oneshot( Request::builder() .uri("/_bridge/dashboard") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let ct = resp.headers().get("content-type").unwrap().to_str().unwrap(); assert!(ct.contains("text/html"), "expected text/html, got {ct}"); let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let html = String::from_utf8(body.to_vec()).unwrap(); assert!(html.contains("OpenFUT Dashboard"), "title missing"); assert!(html.contains("const CORE ="), "core URL injection missing"); // Placeholder URL injected in test mode assert!(html.contains("127.0.0.1:9999"), "core URL not injected"); assert!(!html.contains("{{CORE_URL}}"), "template placeholder was not replaced"); } #[tokio::test] async fn test_dashboard_contains_key_sections() { let app = build_test_app(); let resp = app .oneshot( Request::builder() .uri("/_bridge/dashboard") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let html = String::from_utf8(body.to_vec()).unwrap(); // Verify all major tab sections are present assert!(html.contains("tab-club"), "club tab missing"); assert!(html.contains("tab-collection"), "collection tab missing"); assert!(html.contains("tab-packs"), "packs tab missing"); assert!(html.contains("tab-objectives"), "objectives tab missing"); assert!(html.contains("tab-division"), "division tab missing"); assert!(html.contains("tab-champs"), "fut champs tab missing"); assert!(html.contains("tab-squad"), "squad tab missing"); assert!(html.contains("tab-draft"), "draft tab missing"); assert!(html.contains("tab-market"), "market tab missing"); assert!(html.contains("tab-events"), "events tab missing"); assert!(html.contains("tab-matches"), "matches tab missing"); assert!(html.contains("tab-store"), "pack store tab missing"); assert!(html.contains("tab-sbc"), "sbc tab missing"); assert!(html.contains("tab-statistics"), "statistics tab missing"); assert!(html.contains("tab-catalog"), "card catalog tab missing"); assert!(html.contains("tab-settings"), "settings tab missing"); assert!(html.contains("tab-notifications"), "notifications tab missing"); } #[tokio::test] async fn test_tls_cert_generation() { let result = openfut_bridge::tls::generate_self_signed_cert(); assert!(result.is_ok(), "cert generation failed: {:?}", result.err()); let (cert_pem, key_pem) = result.unwrap(); assert!(!cert_pem.is_empty()); assert!(!key_pem.is_empty()); // Verify the PEM blocks are well-formed let cert_str = String::from_utf8(cert_pem).unwrap(); let key_str = String::from_utf8(key_pem).unwrap(); assert!(cert_str.contains("BEGIN CERTIFICATE")); assert!(key_str.contains("PRIVATE KEY")); } #[tokio::test] async fn test_tls_acceptor_construction() { let (cert_pem, key_pem) = openfut_bridge::tls::generate_self_signed_cert().unwrap(); let acceptor = openfut_bridge::tls::make_tls_acceptor(&cert_pem, &key_pem); assert!(acceptor.is_ok(), "acceptor construction failed: {:?}", acceptor.err()); }