use std::collections::HashMap; use std::str::FromStr; use axum::body::Body; use http_body_util::BodyExt; use tower::ServiceExt; use pom::db; use pom::tools::PomServer; use pom::types::*; #[tokio::test] async fn health_check_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); let snapshot = HealthSnapshot { id: None, target: "test-target".to_string(), status: HealthStatus::Operational, checked_at: "2026-03-10T00:00:00Z".to_string(), response_time_ms: 150, details: Some(HealthDetails { version: Some("1.0.0".to_string()), uptime: Some("5h 30m".to_string()), checks: None, monitoring: None, }), error: None, }; let id = db::insert_health_check(&pool, &snapshot).await.unwrap(); assert!(id > 0); let latest = db::get_latest_health(&pool, "test-target").await.unwrap(); assert!(latest.is_some()); let latest = latest.unwrap(); assert_eq!(latest.status, HealthStatus::Operational); assert_eq!(latest.response_time_ms, 150); assert_eq!(latest.details.unwrap().version.unwrap(), "1.0.0"); } #[tokio::test] async fn health_history_returns_ordered() { let pool = db::connect_in_memory().await.unwrap(); for i in 0..5 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: format!("2026-03-10T0{}:00:00Z", i), response_time_ms: 100 + i * 10, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let history = db::get_health_history(&pool, Some("mnw"), 3).await.unwrap(); assert_eq!(history.len(), 3); // Most recent first (DESC) assert!(history[0].response_time_ms > history[1].response_time_ms); } #[tokio::test] async fn health_history_filters_by_target() { let pool = db::connect_in_memory().await.unwrap(); for target in &["alpha", "beta"] { let snapshot = HealthSnapshot { id: None, target: target.to_string(), status: HealthStatus::Operational, checked_at: "2026-03-10T00:00:00Z".to_string(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let all = db::get_health_history(&pool, None, 10).await.unwrap(); assert_eq!(all.len(), 2); let alpha_only = db::get_health_history(&pool, Some("alpha"), 10).await.unwrap(); assert_eq!(alpha_only.len(), 1); assert_eq!(alpha_only[0].target, "alpha"); } #[tokio::test] async fn test_run_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); let run = TestRun { id: None, target: "mnw".to_string(), started_at: "2026-03-10T00:00:00Z".to_string(), finished_at: Some("2026-03-10T00:02:00Z".to_string()), duration_secs: Some(120), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![ StepResult { name: "cargo check".to_string(), passed: true }, StepResult { name: "cargo test --lib".to_string(), passed: true }, ], total_passed: Some(759), total_failed: Some(0), details: vec![], }, raw_output: "test output here".to_string(), filter: None, }; let id = db::insert_test_run(&pool, &run).await.unwrap(); assert!(id.0 > 0); let latest = db::get_latest_test_run(&pool, "mnw").await.unwrap(); assert!(latest.is_some()); let latest = latest.unwrap(); assert!(latest.passed); assert_eq!(latest.summary.total_passed, Some(759)); assert_eq!(latest.summary.steps.len(), 2); assert_eq!(latest.raw_output, "test output here"); } #[tokio::test] async fn test_history_excludes_other_targets() { let pool = db::connect_in_memory().await.unwrap(); for target in &["mnw", "other"] { let run = TestRun { id: None, target: target.to_string(), started_at: "2026-03-10T00:00:00Z".to_string(), finished_at: None, duration_secs: None, exit_code: None, passed: true, summary: TestSummary { steps: vec![], total_passed: None, total_failed: None, details: vec![] }, raw_output: String::new(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); } let mnw_only = db::get_test_history(&pool, Some("mnw"), 10).await.unwrap(); assert_eq!(mnw_only.len(), 1); } #[tokio::test] async fn prune_removes_old_records() { let pool = db::connect_in_memory().await.unwrap(); // Insert an old health check (60 days ago) let old = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::days(60)).to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &old).await.unwrap(); // Insert a recent one let recent = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &recent).await.unwrap(); let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.health, 1); let remaining = db::get_health_history(&pool, None, 10).await.unwrap(); assert_eq!(remaining.len(), 1); } #[tokio::test] async fn parse_ci_output_integration() { use pom::checks::parse; let output = r#" ======================================== cargo check ======================================== Finished `dev` profile ======================================== cargo test --lib ======================================== running 45 tests test result: ok. 45 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.3s ======================================== CI Summary ======================================== PASS cargo check PASS cargo test --lib PASS cargo clippy All steps passed. "#; let summary = parse::parse_ci_output(output); assert_eq!(summary.steps.len(), 3); assert!(summary.steps.iter().all(|s| s.passed)); assert_eq!(summary.total_passed, Some(45)); assert_eq!(summary.total_failed, Some(0)); } #[tokio::test] async fn peer_identity_first_wins() { let pool = db::connect_in_memory().await.unwrap(); db::store_peer_identity(&pool, "astra", "uuid-1").await.unwrap(); // Second insert with different ID should be ignored (INSERT OR IGNORE) db::store_peer_identity(&pool, "astra", "uuid-2").await.unwrap(); let stored = db::get_peer_identity(&pool, "astra").await.unwrap(); assert_eq!(stored, Some("uuid-1".to_string())); } #[tokio::test] async fn peer_heartbeat_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); db::insert_peer_heartbeat(&pool, "astra", "online", 42).await.unwrap(); db::insert_peer_heartbeat(&pool, "astra", "online", 55).await.unwrap(); db::insert_peer_heartbeat(&pool, "astra", "missing", 0).await.unwrap(); let history = db::get_peer_heartbeat_history(&pool, "astra", 10).await.unwrap(); assert_eq!(history.len(), 3); // Most recent first assert_eq!(history[0].status, "missing"); assert_eq!(history[1].latency_ms, 55); } // --- API endpoint tests --- fn test_config() -> pom::config::Config { toml::from_str( r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" "#, ) .unwrap() } /// GET a path and return (status_code, body_string) — for HTML responses. async fn get_body(app: &axum::Router, path: &str) -> (u16, String) { let req = axum::http::Request::builder() .uri(path) .body(Body::empty()) .unwrap(); let resp = app.clone().oneshot(req).await.unwrap(); let status = resp.status().as_u16(); let body = resp.into_body().collect().await.unwrap().to_bytes(); (status, String::from_utf8_lossy(&body).into_owned()) } fn test_mesh() -> pom::peer::SharedMeshState { let info = pom::peer::InstanceInfo { id: "test-uuid".to_string(), name: "test-node".to_string(), version: "0.1.0".to_string(), targets: vec!["mnw".to_string()], started_at: "2026-03-10T00:00:00Z".to_string(), }; pom::peer::new_mesh_state(info, &HashMap::new()) } async fn api_get(app: &axum::Router, path: &str) -> (u16, serde_json::Value) { let req = axum::http::Request::builder() .uri(path) .body(Body::empty()) .unwrap(); let resp = app.clone().oneshot(req).await.unwrap(); let status = resp.status().as_u16(); let body = resp.into_body().collect().await.unwrap().to_bytes(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); (status, json) } #[tokio::test] async fn api_status_returns_targets() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool.clone(), config, None); // Insert a health check so there's data let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: "2026-03-10T00:00:00Z".to_string(), response_time_ms: 120, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); let (status, json) = api_get(&app, "/api/status").await; assert_eq!(status, 200); assert!(json["targets"]["mnw"].is_object()); assert_eq!(json["targets"]["mnw"]["label"], "MakeNotWork"); assert_eq!(json["targets"]["mnw"]["latest"]["status"], "operational"); assert_eq!(json["targets"]["mnw"]["latest"]["response_time_ms"], 120); } #[tokio::test] async fn api_status_target_not_found() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/status/nonexistent").await; assert_eq!(status, 404); assert!(json["error"].as_str().unwrap().contains("unknown target")); } #[tokio::test] async fn api_peer_info_returns_instance() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let mesh = test_mesh(); let app = pom::api::router(pool, config, Some(mesh)); let (status, json) = api_get(&app, "/api/peer/info").await; assert_eq!(status, 200); assert_eq!(json["id"], "test-uuid"); assert_eq!(json["name"], "test-node"); } #[tokio::test] async fn api_peer_info_disabled_without_mesh() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/peer/info").await; assert_eq!(status, 503); assert!(json["error"].as_str().unwrap().contains("not enabled")); } #[tokio::test] async fn api_mesh_view_includes_self() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let mesh = test_mesh(); let app = pom::api::router(pool, config, Some(mesh)); let (status, json) = api_get(&app, "/api/mesh").await; assert_eq!(status, 200); assert!(json["instances"]["test-node"].is_object()); assert_eq!(json["instances"]["test-node"]["instance"]["id"], "test-uuid"); } // --- Migration tests --- #[tokio::test] async fn migration_fresh_db_reaches_latest_version() { // A fresh in-memory DB should run all migrations and reach the latest version. let pool = db::connect_in_memory().await.unwrap(); let version = db::get_schema_version(&pool).await.unwrap(); assert_eq!(version, 9); // Verify the schema_version table has entries for each migration let rows = sqlx::query_as::<_, (i64, String)>( "SELECT version, description FROM schema_version ORDER BY version", ) .fetch_all(&pool) .await .unwrap(); assert_eq!(rows.len(), 9); assert_eq!(rows[0].0, 1); assert_eq!(rows[0].1, "initial schema"); assert_eq!(rows[1].0, 2); assert_eq!(rows[1].1, "add alerts table"); assert_eq!(rows[2].0, 3); assert_eq!(rows[2].1, "add tls_checks table"); assert_eq!(rows[3].0, 4); assert_eq!(rows[3].1, "add incidents table"); assert_eq!(rows[4].0, 5); assert_eq!(rows[4].1, "add route_checks table"); assert_eq!(rows[5].0, 6); assert_eq!(rows[5].1, "add dns_checks and whois_checks tables"); assert_eq!(rows[6].0, 7); assert_eq!(rows[6].1, "add test_details table"); assert_eq!(rows[7].0, 8); assert_eq!(rows[7].1, "add cors_checks table"); assert_eq!(rows[8].0, 9); assert_eq!(rows[8].1, "add backup_checks table"); // Verify actual tables were created by inserting data let snapshot = HealthSnapshot { id: None, target: "test".to_string(), status: HealthStatus::Operational, checked_at: "2026-03-11T00:00:00Z".to_string(), response_time_ms: 50, details: None, error: None, }; let id = db::insert_health_check(&pool, &snapshot).await.unwrap(); assert!(id > 0); } #[tokio::test] async fn migration_already_current_is_idempotent() { // Running migrations on an already-migrated DB should be a no-op. let pool = db::connect_in_memory().await.unwrap(); assert_eq!(db::get_schema_version(&pool).await.unwrap(), 9); // Run migrations again db::run_migrations(&pool).await.unwrap(); assert_eq!(db::get_schema_version(&pool).await.unwrap(), 9); // schema_version should still have exactly nine entries (not duplicated) let count = sqlx::query_as::<_, (i64,)>("SELECT COUNT(*) FROM schema_version") .fetch_one(&pool) .await .unwrap(); assert_eq!(count.0, 9); } #[tokio::test] async fn migration_detects_pre_migration_database() { // Simulate a pre-migration database: create tables manually without schema_version. let opts = sqlx::sqlite::SqliteConnectOptions::from_str("sqlite::memory:").unwrap(); let pool = sqlx::sqlite::SqlitePoolOptions::new() .max_connections(1) .connect_with(opts) .await .unwrap(); // Create the old-style tables directly (as init_schema used to do) sqlx::query( "CREATE TABLE health_checks ( id INTEGER PRIMARY KEY AUTOINCREMENT, target TEXT NOT NULL, status TEXT NOT NULL, checked_at TEXT NOT NULL, response_time_ms INTEGER NOT NULL, details_json TEXT, error TEXT )", ) .execute(&pool) .await .unwrap(); sqlx::query( "CREATE TABLE test_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, target TEXT NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, duration_secs INTEGER, exit_code INTEGER, passed INTEGER NOT NULL, summary_json TEXT NOT NULL, raw_output TEXT NOT NULL, filter TEXT )", ) .execute(&pool) .await .unwrap(); // Insert some existing data to verify it's preserved sqlx::query( "INSERT INTO health_checks (target, status, checked_at, response_time_ms) VALUES ('mnw', 'operational', '2026-03-10T00:00:00Z', 100)", ) .execute(&pool) .await .unwrap(); // Now run migrations — should detect existing tables, stamp as v1, then run v2+v3+v4+v5+v6 db::run_migrations(&pool).await.unwrap(); // Version should be 9 (stamped v1 + ran v2..v9) assert_eq!(db::get_schema_version(&pool).await.unwrap(), 9); // Description should indicate pre-existing let row = sqlx::query_as::<_, (String,)>( "SELECT description FROM schema_version WHERE version = 1", ) .fetch_one(&pool) .await .unwrap(); assert!(row.0.contains("pre-existing")); // Existing data should be preserved let history = db::get_health_history(&pool, Some("mnw"), 10).await.unwrap(); assert_eq!(history.len(), 1); assert_eq!(history[0].response_time_ms, 100); } // --- MCP tool tests --- fn test_server(pool: sqlx::SqlitePool) -> PomServer { PomServer::new(pool, test_config()) } #[tokio::test] async fn tool_get_status_with_data() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool.clone()); // Insert health + test data let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: "2026-03-10T00:00:00Z".to_string(), response_time_ms: 95, details: Some(HealthDetails { version: Some("2.1.0".to_string()), uptime: Some("3d".to_string()), checks: None, monitoring: None, }), error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); let run = TestRun { id: None, target: "mnw".to_string(), started_at: "2026-03-10T00:00:00Z".to_string(), finished_at: Some("2026-03-10T00:01:00Z".to_string()), duration_secs: Some(60), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![StepResult { name: "cargo test".to_string(), passed: true }], total_passed: Some(100), total_failed: Some(0), details: vec![], }, raw_output: "all good".to_string(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); let result = server.get_status_impl().await.unwrap(); assert!(result.contains("## mnw (MakeNotWork)")); assert!(result.contains("operational")); assert!(result.contains("95ms")); assert!(result.contains("Version: 2.1.0")); assert!(result.contains("Uptime: 3d")); assert!(result.contains("PASSED")); assert!(result.contains("100 passed, 0 failed")); assert!(result.contains("PASS cargo test")); } #[tokio::test] async fn tool_get_status_no_data() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); let result = server.get_status_impl().await.unwrap(); assert!(result.contains("Health: no data")); assert!(result.contains("Tests: no data")); } #[tokio::test] async fn tool_get_status_no_targets() { let pool = db::connect_in_memory().await.unwrap(); let config: pom::config::Config = toml::from_str("").unwrap(); let server = PomServer::new(pool, config); let result = server.get_status_impl().await.unwrap(); assert_eq!(result, "No targets configured."); } #[tokio::test] async fn tool_list_targets() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); let result = server.list_targets_impl().await.unwrap(); let targets: Vec = serde_json::from_str(&result).unwrap(); assert_eq!(targets.len(), 1); assert_eq!(targets[0]["name"], "mnw"); assert_eq!(targets[0]["label"], "MakeNotWork"); assert_eq!(targets[0]["has_health"], true); assert_eq!(targets[0]["has_tests"], false); // test_config has no tests section } #[tokio::test] async fn tool_health_history_with_data() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool.clone()); for i in 0..3 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: format!("2026-03-10T0{i}:00:00Z"), response_time_ms: 100 + i * 10, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let params = pom::tools::health::HealthHistoryParams { target: Some("mnw".to_string()), limit: Some(2), }; let result = server.health_history_impl(params).await.unwrap(); let history: Vec = serde_json::from_str(&result).unwrap(); assert_eq!(history.len(), 2); } #[tokio::test] async fn tool_health_history_empty() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); let params = pom::tools::health::HealthHistoryParams { target: None, limit: None, }; let result = server.health_history_impl(params).await.unwrap(); assert_eq!(result, "No health check history."); } #[tokio::test] async fn tool_health_history_default_limit() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool.clone()); for i in 0..15 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: format!("2026-03-10T{:02}:00:00Z", i), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let params = pom::tools::health::HealthHistoryParams { target: None, limit: None, // should default to 10 }; let result = server.health_history_impl(params).await.unwrap(); let history: Vec = serde_json::from_str(&result).unwrap(); assert_eq!(history.len(), 10); } #[tokio::test] async fn tool_check_health_unknown_target() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); let params = pom::tools::health::CheckHealthParams { target: Some("nonexistent".to_string()), }; let result = server.check_health_impl(params).await.unwrap(); assert_eq!(result, "Unknown target: nonexistent"); } #[tokio::test] async fn tool_test_history_strips_raw_output() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool.clone()); let run = TestRun { id: None, target: "mnw".to_string(), started_at: "2026-03-10T00:00:00Z".to_string(), finished_at: None, duration_secs: None, exit_code: None, passed: true, summary: TestSummary { steps: vec![], total_passed: None, total_failed: None, details: vec![] }, raw_output: "HUGE OUTPUT THAT SHOULD NOT APPEAR".to_string(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); let params = pom::tools::tests::TestHistoryParams { target: Some("mnw".to_string()), limit: None, }; let result = server.test_history_impl(params).await.unwrap(); assert!(!result.contains("HUGE OUTPUT")); assert!(result.contains("mnw")); } #[tokio::test] async fn tool_test_history_empty() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); let params = pom::tools::tests::TestHistoryParams { target: None, limit: None, }; let result = server.test_history_impl(params).await.unwrap(); assert_eq!(result, "No test run history."); } #[tokio::test] async fn tool_last_test_output_returns_raw() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool.clone()); let run = TestRun { id: None, target: "mnw".to_string(), started_at: "2026-03-10T00:00:00Z".to_string(), finished_at: None, duration_secs: None, exit_code: None, passed: true, summary: TestSummary { steps: vec![], total_passed: None, total_failed: None, details: vec![] }, raw_output: "running 42 tests\ntest result: ok".to_string(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); let params = pom::tools::tests::LastTestOutputParams { target: "mnw".to_string(), }; let result = server.last_test_output_impl(params).await.unwrap(); assert_eq!(result, "running 42 tests\ntest result: ok"); } #[tokio::test] async fn tool_last_test_output_no_runs() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); let params = pom::tools::tests::LastTestOutputParams { target: "mnw".to_string(), }; let result = server.last_test_output_impl(params).await.unwrap(); assert_eq!(result, "No test runs found for target 'mnw'"); } #[tokio::test] async fn tool_run_tests_unknown_target() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); let params = pom::tools::tests::RunTestsParams { target: "nonexistent".to_string(), filter: None, }; let result = server.run_tests_impl(params).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Unknown target")); } #[tokio::test] async fn tool_run_tests_no_test_config() { let pool = db::connect_in_memory().await.unwrap(); let server = test_server(pool); // test_config has mnw with health but no tests let params = pom::tools::tests::RunTestsParams { target: "mnw".to_string(), filter: None, }; let result = server.run_tests_impl(params).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("no test configuration")); } // --- Alert tests --- #[tokio::test] async fn migration_v2_creates_alerts_table() { let pool = db::connect_in_memory().await.unwrap(); let version = db::get_schema_version(&pool).await.unwrap(); assert_eq!(version, 9); // Verify alerts table exists by inserting let id = db::insert_alert(&pool, "mnw", "health", Some("operational"), Some("error"), None) .await .unwrap(); assert!(id > 0); } #[tokio::test] async fn alert_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); db::insert_alert(&pool, "health:mnw", "health", Some("operational"), Some("error"), Some("connection refused")) .await .unwrap(); let latest = db::get_latest_alert_for_target(&pool, "health:mnw").await.unwrap(); assert!(latest.is_some()); let row = latest.unwrap(); assert_eq!(row.target, "health:mnw"); assert_eq!(row.alert_type, "health"); assert_eq!(row.from_status.as_deref(), Some("operational")); assert_eq!(row.to_status.as_deref(), Some("error")); assert_eq!(row.error.as_deref(), Some("connection refused")); } #[tokio::test] async fn alert_query_returns_none_for_unknown_target() { let pool = db::connect_in_memory().await.unwrap(); let latest = db::get_latest_alert_for_target(&pool, "nonexistent").await.unwrap(); assert!(latest.is_none()); } #[tokio::test] async fn prune_removes_old_alerts() { let pool = db::connect_in_memory().await.unwrap(); // Insert an old alert directly with old timestamp let old_time = (chrono::Utc::now() - chrono::Duration::days(60)).to_rfc3339(); sqlx::query( "INSERT INTO alerts (target, alert_type, sent_at) VALUES (?, ?, ?)", ) .bind("mnw") .bind("health") .bind(&old_time) .execute(&pool) .await .unwrap(); // Insert a recent alert db::insert_alert(&pool, "mnw", "health", None, None, None).await.unwrap(); let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.alerts, 1); // Recent alert should remain let latest = db::get_latest_alert_for_target(&pool, "mnw").await.unwrap(); assert!(latest.is_some()); } // --- TLS check tests --- #[tokio::test] async fn migration_v3_creates_tls_checks_table() { let pool = db::connect_in_memory().await.unwrap(); let version = db::get_schema_version(&pool).await.unwrap(); assert_eq!(version, 9); // Verify tls_checks table exists by inserting let status = pom::types::TlsStatus { target: "mnw".to_string(), host: "makenot.work".to_string(), port: 443, valid: true, days_remaining: 47, not_before: "2026-01-10T00:00:00Z".to_string(), not_after: "2026-04-27T00:00:00Z".to_string(), subject: "CN=makenot.work".to_string(), issuer: "CN=Let's Encrypt".to_string(), checked_at: "2026-03-11T00:00:00Z".to_string(), error: None, }; let id = db::insert_tls_check(&pool, &status).await.unwrap(); assert!(id > 0); } #[tokio::test] async fn tls_check_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); let status = pom::types::TlsStatus { target: "mnw".to_string(), host: "makenot.work".to_string(), port: 443, valid: true, days_remaining: 47, not_before: "2026-01-10T00:00:00Z".to_string(), not_after: "2026-04-27T00:00:00Z".to_string(), subject: "CN=makenot.work".to_string(), issuer: "CN=Let's Encrypt".to_string(), checked_at: "2026-03-11T00:00:00Z".to_string(), error: None, }; db::insert_tls_check(&pool, &status).await.unwrap(); let latest = db::get_latest_tls_check(&pool, "mnw").await.unwrap(); assert!(latest.is_some()); let row = latest.unwrap(); assert_eq!(row.host, "makenot.work"); assert!(row.valid); assert_eq!(row.days_remaining, 47); assert_eq!(row.subject, "CN=makenot.work"); assert!(row.error.is_none()); } #[tokio::test] async fn tls_check_error_stored() { let pool = db::connect_in_memory().await.unwrap(); let status = pom::types::TlsStatus { target: "mnw".to_string(), host: "makenot.work".to_string(), port: 443, valid: false, days_remaining: 0, not_before: String::new(), not_after: String::new(), subject: String::new(), issuer: String::new(), checked_at: "2026-03-11T00:00:00Z".to_string(), error: Some("connection refused".to_string()), }; db::insert_tls_check(&pool, &status).await.unwrap(); let latest = db::get_latest_tls_check(&pool, "mnw").await.unwrap().unwrap(); assert!(!latest.valid); assert_eq!(latest.error.as_deref(), Some("connection refused")); } #[tokio::test] async fn prune_removes_old_tls_checks() { let pool = db::connect_in_memory().await.unwrap(); // Insert old TLS check let old_time = (chrono::Utc::now() - chrono::Duration::days(60)).to_rfc3339(); sqlx::query( "INSERT INTO tls_checks (target, host, valid, days_remaining, not_before, not_after, subject, issuer, checked_at) VALUES (?, ?, ?, ?, '', '', '', '', ?)", ) .bind("mnw") .bind("makenot.work") .bind(true) .bind(47) .bind(&old_time) .execute(&pool) .await .unwrap(); // Insert recent TLS check let status = pom::types::TlsStatus { target: "mnw".to_string(), host: "makenot.work".to_string(), port: 443, valid: true, days_remaining: 47, not_before: String::new(), not_after: String::new(), subject: String::new(), issuer: String::new(), checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; db::insert_tls_check(&pool, &status).await.unwrap(); let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.tls, 1); // Recent check should remain let latest = db::get_latest_tls_check(&pool, "mnw").await.unwrap(); assert!(latest.is_some()); } #[tokio::test] async fn api_status_target_includes_tls() { let pool = db::connect_in_memory().await.unwrap(); // Config with TLS let config: pom::config::Config = toml::from_str( r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.tls] host = "makenot.work" "#, ) .unwrap(); let app = pom::api::router(pool.clone(), config, None); // Insert TLS check data let status = pom::types::TlsStatus { target: "mnw".to_string(), host: "makenot.work".to_string(), port: 443, valid: true, days_remaining: 47, not_before: "2026-01-10T00:00:00Z".to_string(), not_after: "2026-04-27T00:00:00Z".to_string(), subject: "CN=makenot.work".to_string(), issuer: "CN=Let's Encrypt".to_string(), checked_at: "2026-03-11T00:00:00Z".to_string(), error: None, }; db::insert_tls_check(&pool, &status).await.unwrap(); let (http_status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(http_status, 200); assert!(json["tls"].is_object()); assert_eq!(json["tls"]["host"], "makenot.work"); assert_eq!(json["tls"]["days_remaining"], 47); assert_eq!(json["tls"]["valid"], true); } #[tokio::test] async fn api_status_target_no_tls_omits_field() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); // no TLS config let app = pom::api::router(pool, config, None); let (http_status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(http_status, 200); // tls field should be absent (skip_serializing_if) assert!(json.get("tls").is_none()); } #[tokio::test] async fn config_with_tls_parses() { let toml_str = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tls] host = "makenot.work" port = 8443 warn_days = 30 "#; let config: pom::config::Config = toml::from_str(toml_str).unwrap(); let mnw = config.get_target("mnw").unwrap(); let tls = mnw.tls.as_ref().unwrap(); assert_eq!(tls.host, "makenot.work"); assert_eq!(tls.port, 8443); assert_eq!(tls.warn_days, 30); } #[tokio::test] async fn config_with_alerts_parses() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [alerts] postmark_token = "test-token-123" to = "pom-alerts@makenot.work" cooldown_secs = 120 "#; let config: pom::config::Config = toml::from_str(toml).unwrap(); let alerts = config.alerts.unwrap(); assert_eq!(alerts.postmark_token.as_deref(), Some("test-token-123")); assert_eq!(alerts.to, "pom-alerts@makenot.work"); assert_eq!(alerts.from, "PoM Alerts "); assert_eq!(alerts.cooldown_secs, 120); } // --- Incident tests --- #[tokio::test] async fn migration_v4_creates_incidents_table() { let pool = db::connect_in_memory().await.unwrap(); let version = db::get_schema_version(&pool).await.unwrap(); assert_eq!(version, 9); // Verify incidents table exists by inserting let id = db::insert_incident(&pool, "mnw", "operational", "degraded") .await .unwrap(); assert!(id > 0); } #[tokio::test] async fn incident_insert_and_close_lifecycle() { let pool = db::connect_in_memory().await.unwrap(); // Open an incident let id = db::insert_incident(&pool, "mnw", "operational", "degraded") .await .unwrap(); assert!(id > 0); // Should be visible as open let open = db::get_open_incident(&pool, "mnw").await.unwrap(); assert!(open.is_some()); let open = open.unwrap(); assert_eq!(open.from_status, "operational"); assert_eq!(open.to_status, "degraded"); assert!(open.ended_at.is_none()); // Close it let closed_count = db::close_open_incidents(&pool, "mnw").await.unwrap(); assert_eq!(closed_count, 1); // No more open incidents let open = db::get_open_incident(&pool, "mnw").await.unwrap(); assert!(open.is_none()); // Recent incidents should include the closed one let recent = db::get_recent_incidents(&pool, "mnw", 10).await.unwrap(); assert_eq!(recent.len(), 1); assert!(recent[0].ended_at.is_some()); assert!(recent[0].duration_secs.is_some()); } #[tokio::test] async fn incident_close_only_affects_target() { let pool = db::connect_in_memory().await.unwrap(); db::insert_incident(&pool, "mnw", "operational", "error").await.unwrap(); db::insert_incident(&pool, "other", "operational", "error").await.unwrap(); // Close only mnw db::close_open_incidents(&pool, "mnw").await.unwrap(); assert!(db::get_open_incident(&pool, "mnw").await.unwrap().is_none()); assert!(db::get_open_incident(&pool, "other").await.unwrap().is_some()); } #[tokio::test] async fn prune_removes_closed_incidents_only() { let pool = db::connect_in_memory().await.unwrap(); // Insert an old closed incident let old_time = (chrono::Utc::now() - chrono::Duration::days(60)).to_rfc3339(); sqlx::query( "INSERT INTO incidents (target, started_at, ended_at, duration_secs, from_status, to_status) VALUES (?, ?, ?, 3600, 'operational', 'error')", ) .bind("mnw") .bind(&old_time) .bind(&old_time) .execute(&pool) .await .unwrap(); // Insert an old open incident (should NOT be pruned) sqlx::query( "INSERT INTO incidents (target, started_at, from_status, to_status) VALUES (?, ?, 'operational', 'error')", ) .bind("mnw") .bind(&old_time) .execute(&pool) .await .unwrap(); let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.incidents, 1); // only the closed one // The open incident should remain let remaining = db::get_recent_incidents(&pool, "mnw", 10).await.unwrap(); assert_eq!(remaining.len(), 1); assert!(remaining[0].ended_at.is_none()); } #[tokio::test] async fn api_status_includes_incidents() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool.clone(), config, None); // Insert an open incident db::insert_incident(&pool, "mnw", "operational", "degraded").await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json["current_incident"].is_object()); assert_eq!(json["current_incident"]["from_status"], "operational"); assert_eq!(json["current_incident"]["to_status"], "degraded"); assert!(!json["incidents"].as_array().unwrap().is_empty()); } #[tokio::test] async fn api_status_no_incidents_omits_fields() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); // current_incident and incidents should be absent assert!(json.get("current_incident").is_none()); assert!(json.get("incidents").is_none()); } // --- Route check tests --- #[tokio::test] async fn migration_v5_creates_route_checks_table() { let pool = db::connect_in_memory().await.unwrap(); let version = db::get_schema_version(&pool).await.unwrap(); assert_eq!(version, 9); // Verify route_checks table exists by inserting let result = pom::checks::routes::RouteCheckResult { target: "mnw".to_string(), path: "/".to_string(), status_code: 200, ok: true, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 50, error: None, }; let id = db::insert_route_check(&pool, &result).await.unwrap(); assert!(id > 0); } #[tokio::test] async fn route_check_insert_and_latest_query() { let pool = db::connect_in_memory().await.unwrap(); // Insert two checks for different paths let r1 = pom::checks::routes::RouteCheckResult { target: "mnw".to_string(), path: "/".to_string(), status_code: 200, ok: true, checked_at: "2026-03-13T00:00:00Z".to_string(), response_time_ms: 50, error: None, }; let r2 = pom::checks::routes::RouteCheckResult { target: "mnw".to_string(), path: "/docs".to_string(), status_code: 404, ok: false, checked_at: "2026-03-13T00:00:00Z".to_string(), response_time_ms: 30, error: Some("HTTP 404".to_string()), }; db::insert_route_check(&pool, &r1).await.unwrap(); db::insert_route_check(&pool, &r2).await.unwrap(); // Insert a newer check for "/" (should supersede the first) let r3 = pom::checks::routes::RouteCheckResult { target: "mnw".to_string(), path: "/".to_string(), status_code: 200, ok: true, checked_at: "2026-03-13T01:00:00Z".to_string(), response_time_ms: 45, error: None, }; db::insert_route_check(&pool, &r3).await.unwrap(); let latest = db::get_latest_route_checks(&pool, "mnw").await.unwrap(); assert_eq!(latest.len(), 2); // "/" should have the newer check (45ms) let root = latest.iter().find(|r| r.path == "/").unwrap(); assert_eq!(root.response_time_ms, 45); assert!(root.ok); // "/docs" should still show 404 let docs = latest.iter().find(|r| r.path == "/docs").unwrap(); assert!(!docs.ok); assert_eq!(docs.status_code, 404); } #[tokio::test] async fn route_check_prune() { let pool = db::connect_in_memory().await.unwrap(); let old = pom::checks::routes::RouteCheckResult { target: "mnw".to_string(), path: "/".to_string(), status_code: 200, ok: true, checked_at: "2020-01-01T00:00:00Z".to_string(), response_time_ms: 50, error: None, }; db::insert_route_check(&pool, &old).await.unwrap(); let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.routes, 1); } #[tokio::test] async fn api_status_includes_route_status() { let pool = db::connect_in_memory().await.unwrap(); let config: pom::config::Config = toml::from_str(r#" [targets.mnw] label = "MakeNotWork" expected_routes = ["/"] [targets.mnw.health] url = "https://makenot.work/health" "#).unwrap(); let app = pom::api::router(pool.clone(), config, None); // Insert route checks let r1 = pom::checks::routes::RouteCheckResult { target: "mnw".to_string(), path: "/".to_string(), status_code: 200, ok: true, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 50, error: None, }; db::insert_route_check(&pool, &r1).await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); let routes = json["route_status"].as_array().unwrap(); assert_eq!(routes.len(), 1); assert_eq!(routes[0]["path"], "/"); assert_eq!(routes[0]["ok"], true); } #[tokio::test] async fn api_status_omits_empty_route_status() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool.clone(), config, None); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); // route_status should be omitted when empty (skip_serializing_if) assert!(json.get("route_status").is_none()); } // --- Latency trending tests --- #[tokio::test] async fn get_response_times_returns_ordered_data() { let pool = db::connect_in_memory().await.unwrap(); for i in 0..5 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: format!("2026-03-10T0{}:00:00+00:00", i), response_time_ms: 100 + i * 10, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let times = db::get_response_times(&pool, "mnw", "2026-03-10T00:00:00+00:00").await.unwrap(); assert_eq!(times.len(), 5); // Verify ASC ordering assert!(times[0].1 <= times[4].1); } #[tokio::test] async fn get_recent_response_times_filters_operational_only() { let pool = db::connect_in_memory().await.unwrap(); // Insert operational checks for i in 0..3 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: format!("2026-03-10T0{}:00:00Z", i), response_time_ms: 100 + i * 10, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } // Insert non-operational checks let error_snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Error, checked_at: "2026-03-10T03:00:00Z".to_string(), response_time_ms: 5000, details: None, error: Some("timeout".to_string()), }; db::insert_health_check(&pool, &error_snapshot).await.unwrap(); let times = db::get_recent_response_times(&pool, "mnw", 10).await.unwrap(); assert_eq!(times.len(), 3); // only operational // All should be our operational values, not the 5000ms error assert!(times.iter().all(|&t| t < 5000)); } #[tokio::test] async fn api_trends_returns_buckets() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool.clone(), config, None); // Insert hourly data points for i in 0..5 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::hours(i)).to_rfc3339(), response_time_ms: 100 + i * 20, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let (status, json) = api_get(&app, "/api/trends/mnw?hours=24&bucket_minutes=60").await; assert_eq!(status, 200); assert_eq!(json["target"], "mnw"); assert_eq!(json["window_hours"], 24); assert_eq!(json["bucket_minutes"], 60); assert!(!json["buckets"].as_array().unwrap().is_empty()); assert!(json["overall"].is_object()); } #[tokio::test] async fn api_trends_nonexistent_target() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/trends/nonexistent").await; assert_eq!(status, 404); assert!(json["error"].as_str().unwrap().contains("unknown target")); } #[tokio::test] async fn api_status_includes_latency_24h_with_data() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool.clone(), config, None); // Insert recent operational check let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 120, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json["latency_24h"].is_object()); assert_eq!(json["latency_24h"]["min_ms"], 120); assert_eq!(json["latency_24h"]["sample_count"], 1); } #[tokio::test] async fn api_status_omits_latency_24h_when_no_data() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); // latency_24h should be absent (skip_serializing_if) assert!(json.get("latency_24h").is_none()); } #[tokio::test] async fn config_trending_parses() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.health.trending] baseline_window_hours = 48 spike_threshold = 1.5 "#; let config: pom::config::Config = toml::from_str(toml).unwrap(); let trending = config.get_target("mnw").unwrap().health.as_ref().unwrap().trending.as_ref().unwrap(); assert_eq!(trending.baseline_window_hours, 48); assert_eq!(trending.spike_threshold, 1.5); } #[tokio::test] async fn config_with_health_expect_parses() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.health.expect] status_code = 200 json_fields = { "status" = "operational" } "#; let config: pom::config::Config = toml::from_str(toml).unwrap(); let expect = config.get_target("mnw").unwrap().health.as_ref().unwrap().expect.as_ref().unwrap(); assert_eq!(expect.status_code, Some(200)); assert_eq!(expect.json_fields.get("status").unwrap(), "operational"); } // --- Test staleness tests --- fn test_config_with_tests() -> pom::config::Config { toml::from_str( r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [targets.mnw.tests] ssh = "max@host" command = "./ci.sh" staleness_days = 7 "#, ) .unwrap() } #[tokio::test] async fn get_version_at_time_returns_version() { let pool = db::connect_in_memory().await.unwrap(); // Insert a health check with version details let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: "2026-03-10T00:00:00Z".to_string(), response_time_ms: 95, details: Some(HealthDetails { version: Some("0.1.8".to_string()), uptime: None, checks: None, monitoring: None, }), error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); let version = db::get_version_at_time(&pool, "mnw", "2026-03-10T01:00:00Z") .await .unwrap(); assert_eq!(version, Some("0.1.8".to_string())); } #[tokio::test] async fn get_version_at_time_returns_none_when_no_data() { let pool = db::connect_in_memory().await.unwrap(); let version = db::get_version_at_time(&pool, "mnw", "2026-03-10T01:00:00Z") .await .unwrap(); assert!(version.is_none()); } #[tokio::test] async fn api_status_includes_staleness_version_change() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config_with_tests(); let app = pom::api::router(pool.clone(), config, None); // Insert health check with version 0.1.8 before test run let old_health = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: "2026-03-09T00:00:00Z".to_string(), response_time_ms: 95, details: Some(HealthDetails { version: Some("0.1.8".to_string()), uptime: None, checks: None, monitoring: None, }), error: None, }; db::insert_health_check(&pool, &old_health).await.unwrap(); // Insert test run at a time when version was 0.1.8 let run = TestRun { id: None, target: "mnw".to_string(), started_at: chrono::Utc::now().to_rfc3339(), finished_at: None, duration_secs: Some(60), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: Some(100), total_failed: Some(0), details: vec![] }, raw_output: String::new(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); // Insert current health check with version 0.1.9 let new_health = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 95, details: Some(HealthDetails { version: Some("0.1.9".to_string()), uptime: None, checks: None, monitoring: None, }), error: None, }; db::insert_health_check(&pool, &new_health).await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json["test_staleness"].is_object()); assert_eq!(json["test_staleness"]["stale"], true); let reason = json["test_staleness"]["reason"].as_str().unwrap(); assert!(reason.contains("version changed"), "reason was: {reason}"); } #[tokio::test] async fn api_status_includes_staleness_by_age() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config_with_tests(); let app = pom::api::router(pool.clone(), config, None); // Insert old health check let old_time = (chrono::Utc::now() - chrono::Duration::days(10)).to_rfc3339(); let health = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: old_time.clone(), response_time_ms: 95, details: Some(HealthDetails { version: Some("0.1.9".to_string()), uptime: None, checks: None, monitoring: None, }), error: None, }; db::insert_health_check(&pool, &health).await.unwrap(); // Insert old test run (10 days ago, same version) let run = TestRun { id: None, target: "mnw".to_string(), started_at: old_time, finished_at: None, duration_secs: Some(60), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: Some(100), total_failed: Some(0), details: vec![] }, raw_output: String::new(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); // Insert current health (same version) let current_health = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 95, details: Some(HealthDetails { version: Some("0.1.9".to_string()), uptime: None, checks: None, monitoring: None, }), error: None, }; db::insert_health_check(&pool, ¤t_health).await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json["test_staleness"].is_object()); assert_eq!(json["test_staleness"]["stale"], true); let reason = json["test_staleness"]["reason"].as_str().unwrap(); assert!(reason.contains("days old"), "reason was: {reason}"); } #[tokio::test] async fn api_status_not_stale_when_fresh() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config_with_tests(); let app = pom::api::router(pool.clone(), config, None); // Insert health check with version let health = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 95, details: Some(HealthDetails { version: Some("0.1.9".to_string()), uptime: None, checks: None, monitoring: None, }), error: None, }; db::insert_health_check(&pool, &health).await.unwrap(); // Insert recent test run let run = TestRun { id: None, target: "mnw".to_string(), started_at: chrono::Utc::now().to_rfc3339(), finished_at: None, duration_secs: Some(60), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: Some(100), total_failed: Some(0), details: vec![] }, raw_output: String::new(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json["test_staleness"].is_object()); assert_eq!(json["test_staleness"]["stale"], false); } #[tokio::test] async fn config_staleness_days_parses() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tests] ssh = "host" command = "./ci.sh" staleness_days = 14 "#; let config: pom::config::Config = toml::from_str(toml).unwrap(); assert_eq!(config.get_target("mnw").unwrap().tests.as_ref().unwrap().staleness_days, 14); } #[tokio::test] async fn tool_get_status_shows_staleness() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config_with_tests(); let server = PomServer::new(pool.clone(), config); // No test data — should show stale let result = server.get_status_impl().await.unwrap(); assert!(result.contains("STALE"), "output was: {result}"); assert!(result.contains("no tests have been run"), "output was: {result}"); } #[tokio::test] async fn api_status_no_staleness_without_tests_config() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); // no tests section let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); // test_staleness should be absent when no tests config assert!(json.get("test_staleness").is_none()); } // --- Prune days=0 guard tests --- #[tokio::test] async fn prune_with_days_zero_is_noop() { let pool = db::connect_in_memory().await.unwrap(); // Insert a recent health check let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); // Prune with days=0 should delete nothing let result = db::prune_old_records(&pool, 0).await.unwrap(); assert_eq!(result.health, 0); assert_eq!(result.tests, 0); assert_eq!(result.heartbeats, 0); assert_eq!(result.alerts, 0); assert_eq!(result.tls, 0); assert_eq!(result.incidents, 0); assert_eq!(result.routes, 0); assert_eq!(result.dns, 0); assert_eq!(result.whois, 0); // Records should still exist let remaining = db::get_health_history(&pool, None, 10).await.unwrap(); assert_eq!(remaining.len(), 1); } #[tokio::test] async fn prune_with_days_seven_keeps_recent() { let pool = db::connect_in_memory().await.unwrap(); // Insert a health check from yesterday let yesterday = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::days(1)).to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &yesterday).await.unwrap(); // Insert a health check from 10 days ago let old = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::days(10)).to_rfc3339(), response_time_ms: 200, details: None, error: None, }; db::insert_health_check(&pool, &old).await.unwrap(); // Prune with days=7 should only delete the 10-day-old record let result = db::prune_old_records(&pool, 7).await.unwrap(); assert_eq!(result.health, 1); // Yesterday's record should remain let remaining = db::get_health_history(&pool, None, 10).await.unwrap(); assert_eq!(remaining.len(), 1); assert_eq!(remaining[0].response_time_ms, 100); } #[tokio::test] async fn prune_with_days_one_keeps_today() { let pool = db::connect_in_memory().await.unwrap(); // Insert a health check from now let today = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: chrono::Utc::now().to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &today).await.unwrap(); // Insert a health check from 2 days ago let old = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::days(2)).to_rfc3339(), response_time_ms: 200, details: None, error: None, }; db::insert_health_check(&pool, &old).await.unwrap(); // Prune with days=1 should delete the 2-day-old record, keep today's let result = db::prune_old_records(&pool, 1).await.unwrap(); assert_eq!(result.health, 1); let remaining = db::get_health_history(&pool, None, 10).await.unwrap(); assert_eq!(remaining.len(), 1); assert_eq!(remaining[0].response_time_ms, 100); } // --- SSH timeout_secs config test --- #[test] fn ssh_config_timeout_secs_is_parsed() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tests] ssh = "hetzner" command = "./ci.sh" timeout_secs = 5 "#; let config: pom::config::Config = toml::from_str(toml).unwrap(); let tests = config.get_target("mnw").unwrap().tests.as_ref().unwrap(); assert_eq!(tests.timeout_secs, 5); } #[test] fn ssh_config_timeout_secs_default() { let toml = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.tests] ssh = "hetzner" command = "./ci.sh" "#; let config: pom::config::Config = toml::from_str(toml).unwrap(); let tests = config.get_target("mnw").unwrap().tests.as_ref().unwrap(); assert_eq!(tests.timeout_secs, 600); // default } // --- Alert cooldown key consistency test --- #[tokio::test] async fn alert_cooldown_key_matches_across_send_and_check() { let pool = db::connect_in_memory().await.unwrap(); let config = pom::config::AlertConfig { postmark_token: None, to: "test@example.com".to_string(), from: "PoM Alerts ".to_string(), cooldown_secs: 300, wam_url: None, }; let alerter = pom::alerts::Alerter::new(config, pool.clone(), "test".to_string()); // Send a health alert for target "example.com" alerter.send_health_alert("example.com", "Example", "operational", "error", None).await; // The alert should be recorded with key "health:example.com" let alert = db::get_latest_alert_for_target(&pool, "health:example.com").await.unwrap(); assert!(alert.is_some(), "alert should be recorded with prefixed key"); // The old (bare) key should have no record let bare = db::get_latest_alert_for_target(&pool, "example.com").await.unwrap(); assert!(bare.is_none(), "no alert should exist under bare target name"); } // --- check_health integration tests (mock HTTP server) --- #[tokio::test] async fn check_health_operational_json_response() { use axum::routing::get; use pom::checks::http::check_health; use pom::config::HealthConfig; let app = axum::Router::new().route("/health", get(|| async { axum::Json(serde_json::json!({ "status": "operational", "version": "1.0.0", "uptime": "2d 5h", })) })); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); let config = HealthConfig { url: format!("http://{addr}/health"), timeout_secs: 5, interval_secs: None, expect: None, trending: None, }; let snapshot = check_health("test", &config, None).await; assert_eq!(snapshot.status, HealthStatus::Operational); assert!(snapshot.response_time_ms >= 0); let details = snapshot.details.unwrap(); assert_eq!(details.version.as_deref(), Some("1.0.0")); assert_eq!(details.uptime.as_deref(), Some("2d 5h")); assert!(snapshot.error.is_none()); } #[tokio::test] async fn check_health_degraded_unknown_status() { use axum::routing::get; use pom::checks::http::check_health; use pom::config::HealthConfig; let app = axum::Router::new().route("/health", get(|| async { axum::Json(serde_json::json!({ "status": "starting_up", "version": "1.0.0", })) })); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); let config = HealthConfig { url: format!("http://{addr}/health"), timeout_secs: 5, interval_secs: None, expect: None, trending: None, }; let snapshot = check_health("test", &config, None).await; assert_eq!(snapshot.status, HealthStatus::Degraded); } #[tokio::test] async fn check_health_unreachable_target() { use pom::checks::http::check_health; use pom::config::HealthConfig; let config = HealthConfig { url: "http://127.0.0.1:19999/health".to_string(), timeout_secs: 1, interval_secs: None, expect: None, trending: None, }; let snapshot = check_health("test", &config, None).await; assert_eq!(snapshot.status, HealthStatus::Unreachable); assert!(snapshot.error.is_some()); } #[tokio::test] async fn check_health_with_expectations_passing() { use axum::routing::get; use pom::checks::http::check_health; use pom::config::{HealthConfig, HealthExpectation}; let app = axum::Router::new().route("/health", get(|| async { axum::Json(serde_json::json!({ "status": "operational", "version": "1.0.0", })) })); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); let expect = HealthExpectation { status_code: Some(200), json_fields: [("status".to_string(), "operational".to_string())].into(), body_contains: None, }; let config = HealthConfig { url: format!("http://{addr}/health"), timeout_secs: 5, interval_secs: None, expect: Some(expect.clone()), trending: None, }; let snapshot = check_health("test", &config, Some(&expect)).await; assert_eq!(snapshot.status, HealthStatus::Operational); assert!(snapshot.error.is_none()); } #[tokio::test] async fn check_health_with_expectations_failing() { use axum::routing::get; use pom::checks::http::check_health; use pom::config::{HealthConfig, HealthExpectation}; let app = axum::Router::new().route("/health", get(|| async { axum::Json(serde_json::json!({ "status": "degraded", "version": "1.0.0", })) })); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); let expect = HealthExpectation { status_code: Some(200), json_fields: [("status".to_string(), "operational".to_string())].into(), body_contains: None, }; let config = HealthConfig { url: format!("http://{addr}/health"), timeout_secs: 5, interval_secs: None, expect: Some(expect.clone()), trending: None, }; let snapshot = check_health("test", &config, Some(&expect)).await; assert_eq!(snapshot.status, HealthStatus::Degraded); assert!(snapshot.error.is_some()); assert!(snapshot.error.unwrap().contains("expected \"operational\"")); } // --- check_tls integration test (self-signed cert) --- #[tokio::test] async fn check_tls_with_test_cert() { use pom::checks::tls::check_tls; use pom::config::TlsConfig; use rcgen::generate_simple_self_signed; use tokio_rustls::rustls; // Install crypto provider (tests don't go through main()) let _ = rustls::crypto::ring::default_provider().install_default(); // Generate a self-signed cert let subject_alt_names = vec!["localhost".to_string()]; let cert = generate_simple_self_signed(subject_alt_names).unwrap(); let cert_der = cert.cert.der().clone(); let key_der = cert.signing_key.serialize_der(); // Start a TLS server let server_config = rustls::ServerConfig::builder() .with_no_client_auth() .with_single_cert( vec![rustls_pki_types::CertificateDer::from(cert_der.to_vec())], rustls_pki_types::PrivateKeyDer::try_from(key_der).unwrap(), ) .unwrap(); let acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(server_config)); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); tokio::spawn(async move { // Accept one connection to prove TLS works if let Ok((stream, _)) = listener.accept().await { let _ = acceptor.accept(stream).await; } }); let tls_config = TlsConfig { host: "localhost".to_string(), port, warn_days: 14, }; let result = check_tls("test", &tls_config).await; // Self-signed cert won't pass webpki validation, so this should return an error // but it should still complete without panic assert_eq!(result.target, "test"); assert!(!result.checked_at.is_empty()); // Self-signed cert → error expected (webpki root store doesn't include it) assert!(result.error.is_some() || result.valid); } // --- API health endpoint test --- #[tokio::test] async fn api_health_endpoint_returns_operational() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/health").await; assert_eq!(status, 200); assert_eq!(json["status"], "operational"); assert!(json["version"].as_str().is_some()); } #[tokio::test] async fn api_health_endpoint_no_auth_required() { let pool = db::connect_in_memory().await.unwrap(); // Config with auth token — health endpoint should still be accessible let mut config = test_config(); config.serve.api_token = Some("secret123".to_string()); let app = pom::api::router(pool, config, None); // No auth header, but /api/health should still work let (status, json) = api_get(&app, "/api/health").await; assert_eq!(status, 200); assert_eq!(json["status"], "operational"); } // --- Rate limit test --- #[tokio::test] async fn api_rate_limit_rejects_excess_requests() { use pom::api::RateLimiter; let limiter = RateLimiter::new(3, std::time::Duration::from_secs(60)); assert!(limiter.try_acquire()); // 1 assert!(limiter.try_acquire()); // 2 assert!(limiter.try_acquire()); // 3 assert!(!limiter.try_acquire()); // 4 — should be rejected } // --- Peer UUID mismatch state reset test --- #[tokio::test] async fn peer_uuid_mismatch_updates_db_identity() { let pool = db::connect_in_memory().await.unwrap(); // Store initial identity db::store_peer_identity(&pool, "peer1", "old-uuid").await.unwrap(); let stored = db::get_peer_identity(&pool, "peer1").await.unwrap(); assert_eq!(stored, Some("old-uuid".to_string())); // Update identity (simulating what happens on UUID mismatch) db::update_peer_identity(&pool, "peer1", "new-uuid").await.unwrap(); let stored = db::get_peer_identity(&pool, "peer1").await.unwrap(); assert_eq!(stored, Some("new-uuid".to_string())); } // --- DNS check tests --- #[tokio::test] async fn migration_v6_creates_dns_and_whois_tables() { let pool = db::connect_in_memory().await.unwrap(); let version = db::get_schema_version(&pool).await.unwrap(); assert_eq!(version, 9); // Verify dns_checks table exists let dns_result = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.144.244".to_string()], actual: vec!["5.78.144.244".to_string()], matches: true, checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; let id = db::insert_dns_check(&pool, &dns_result).await.unwrap(); assert!(id > 0); // Verify whois_checks table exists let whois_result = WhoisResult { target: "mnw".to_string(), domain: "makenot.work".to_string(), registrar: Some("Namecheap, Inc.".to_string()), expiry_date: Some("2026-12-01T12:00:00Z".to_string()), days_remaining: Some(261), nameservers: vec!["ns1.example.com".to_string()], checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; let id = db::insert_whois_check(&pool, &whois_result).await.unwrap(); assert!(id > 0); } #[tokio::test] async fn dns_check_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); let result = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.144.244".to_string()], actual: vec!["5.78.144.244".to_string()], matches: true, checked_at: "2026-03-15T00:00:00Z".to_string(), error: None, }; db::insert_dns_check(&pool, &result).await.unwrap(); let latest = db::get_latest_dns_checks(&pool, "mnw").await.unwrap(); assert_eq!(latest.len(), 1); assert_eq!(latest[0].name, "makenot.work"); assert_eq!(latest[0].record_type, "A"); assert!(latest[0].matches); } #[tokio::test] async fn dns_check_latest_per_name_and_type() { let pool = db::connect_in_memory().await.unwrap(); // Insert two checks for same name/type, different times let r1 = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["1.2.3.4".to_string()], actual: vec!["5.6.7.8".to_string()], matches: false, checked_at: "2026-03-15T00:00:00Z".to_string(), error: None, }; let r2 = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.144.244".to_string()], actual: vec!["5.78.144.244".to_string()], matches: true, checked_at: "2026-03-15T01:00:00Z".to_string(), error: None, }; db::insert_dns_check(&pool, &r1).await.unwrap(); db::insert_dns_check(&pool, &r2).await.unwrap(); // Should return only the latest check per name+type let latest = db::get_latest_dns_checks(&pool, "mnw").await.unwrap(); assert_eq!(latest.len(), 1); assert!(latest[0].matches); } #[tokio::test] async fn dns_check_multiple_records() { let pool = db::connect_in_memory().await.unwrap(); let r1 = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.144.244".to_string()], actual: vec!["5.78.144.244".to_string()], matches: true, checked_at: "2026-03-15T00:00:00Z".to_string(), error: None, }; let r2 = DnsCheckResult { target: "mnw".to_string(), name: "forums.makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.144.244".to_string()], actual: vec!["5.78.144.244".to_string()], matches: true, checked_at: "2026-03-15T00:00:00Z".to_string(), error: None, }; db::insert_dns_check(&pool, &r1).await.unwrap(); db::insert_dns_check(&pool, &r2).await.unwrap(); let latest = db::get_latest_dns_checks(&pool, "mnw").await.unwrap(); assert_eq!(latest.len(), 2); } #[tokio::test] async fn dns_check_filters_by_target() { let pool = db::connect_in_memory().await.unwrap(); let r1 = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.144.244".to_string()], actual: vec!["5.78.144.244".to_string()], matches: true, checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; let r2 = DnsCheckResult { target: "htpy".to_string(), name: "htpy.app".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.135.189".to_string()], actual: vec!["5.78.135.189".to_string()], matches: true, checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; db::insert_dns_check(&pool, &r1).await.unwrap(); db::insert_dns_check(&pool, &r2).await.unwrap(); let mnw_checks = db::get_latest_dns_checks(&pool, "mnw").await.unwrap(); assert_eq!(mnw_checks.len(), 1); assert_eq!(mnw_checks[0].name, "makenot.work"); } // --- WHOIS check tests --- #[tokio::test] async fn whois_check_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); let result = WhoisResult { target: "mnw".to_string(), domain: "makenot.work".to_string(), registrar: Some("Namecheap, Inc.".to_string()), expiry_date: Some("2026-12-01T12:00:00Z".to_string()), days_remaining: Some(261), nameservers: vec!["dns1.registrar-servers.com".to_string()], checked_at: "2026-03-15T00:00:00Z".to_string(), error: None, }; db::insert_whois_check(&pool, &result).await.unwrap(); let latest = db::get_latest_whois_check(&pool, "mnw").await.unwrap(); assert!(latest.is_some()); let row = latest.unwrap(); assert_eq!(row.domain, "makenot.work"); assert_eq!(row.registrar.as_deref(), Some("Namecheap, Inc.")); assert_eq!(row.days_remaining, Some(261)); } #[tokio::test] async fn whois_check_returns_latest() { let pool = db::connect_in_memory().await.unwrap(); let r1 = WhoisResult { target: "mnw".to_string(), domain: "makenot.work".to_string(), registrar: Some("Old Registrar".to_string()), expiry_date: Some("2026-06-01T00:00:00Z".to_string()), days_remaining: Some(78), nameservers: vec![], checked_at: "2026-03-15T00:00:00Z".to_string(), error: None, }; let r2 = WhoisResult { target: "mnw".to_string(), domain: "makenot.work".to_string(), registrar: Some("New Registrar".to_string()), expiry_date: Some("2027-06-01T00:00:00Z".to_string()), days_remaining: Some(443), nameservers: vec![], checked_at: "2026-03-15T01:00:00Z".to_string(), error: None, }; db::insert_whois_check(&pool, &r1).await.unwrap(); db::insert_whois_check(&pool, &r2).await.unwrap(); let latest = db::get_latest_whois_check(&pool, "mnw").await.unwrap().unwrap(); assert_eq!(latest.registrar.as_deref(), Some("New Registrar")); assert_eq!(latest.days_remaining, Some(443)); } #[tokio::test] async fn whois_check_returns_none_for_unknown_target() { let pool = db::connect_in_memory().await.unwrap(); let latest = db::get_latest_whois_check(&pool, "nonexistent").await.unwrap(); assert!(latest.is_none()); } #[tokio::test] async fn whois_check_error_stored() { let pool = db::connect_in_memory().await.unwrap(); let result = WhoisResult { target: "mnw".to_string(), domain: "makenot.work".to_string(), registrar: None, expiry_date: None, days_remaining: None, nameservers: vec![], checked_at: chrono::Utc::now().to_rfc3339(), error: Some("WHOIS connection timed out".to_string()), }; db::insert_whois_check(&pool, &result).await.unwrap(); let latest = db::get_latest_whois_check(&pool, "mnw").await.unwrap().unwrap(); assert_eq!(latest.error.as_deref(), Some("WHOIS connection timed out")); assert!(latest.registrar.is_none()); } #[tokio::test] async fn cors_check_insert_and_query() { let pool = db::connect_in_memory().await.unwrap(); let result = CorsCheckResult { target: "mnw".to_string(), url: "https://storage.example.com/bucket/probe".to_string(), origin: "https://makenot.work".to_string(), method: "PUT".to_string(), passes: true, checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; db::insert_cors_check(&pool, &result).await.unwrap(); let latest = db::get_latest_cors_checks(&pool, "mnw").await.unwrap(); assert_eq!(latest.len(), 1); assert!(latest[0].passes); assert_eq!(latest[0].url, "https://storage.example.com/bucket/probe"); } #[tokio::test] async fn cors_check_latest_per_url() { let pool = db::connect_in_memory().await.unwrap(); // Insert an old failing check let r1 = CorsCheckResult { target: "mnw".to_string(), url: "https://storage.example.com/bucket/probe".to_string(), origin: "https://makenot.work".to_string(), method: "PUT".to_string(), passes: false, checked_at: "2026-03-01T00:00:00Z".to_string(), error: Some("Missing Access-Control-Allow-Origin".to_string()), }; db::insert_cors_check(&pool, &r1).await.unwrap(); // Insert a newer passing check for same URL let r2 = CorsCheckResult { target: "mnw".to_string(), url: "https://storage.example.com/bucket/probe".to_string(), origin: "https://makenot.work".to_string(), method: "PUT".to_string(), passes: true, checked_at: "2026-03-15T00:00:00Z".to_string(), error: None, }; db::insert_cors_check(&pool, &r2).await.unwrap(); let latest = db::get_latest_cors_checks(&pool, "mnw").await.unwrap(); // Should return only the latest (passing) check per URL assert_eq!(latest.len(), 1); assert!(latest[0].passes); } #[tokio::test] async fn cors_check_filters_by_target() { let pool = db::connect_in_memory().await.unwrap(); let r1 = CorsCheckResult { target: "mnw".to_string(), url: "https://storage.example.com/bucket/probe".to_string(), origin: "https://makenot.work".to_string(), method: "PUT".to_string(), passes: true, checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; let r2 = CorsCheckResult { target: "other".to_string(), url: "https://other.example.com/probe".to_string(), origin: "https://other.app".to_string(), method: "PUT".to_string(), passes: false, checked_at: chrono::Utc::now().to_rfc3339(), error: Some("Failed".to_string()), }; db::insert_cors_check(&pool, &r1).await.unwrap(); db::insert_cors_check(&pool, &r2).await.unwrap(); let mnw_checks = db::get_latest_cors_checks(&pool, "mnw").await.unwrap(); assert_eq!(mnw_checks.len(), 1); assert_eq!(mnw_checks[0].target, "mnw"); } #[tokio::test] async fn prune_removes_old_dns_checks() { let pool = db::connect_in_memory().await.unwrap(); let old_time = (chrono::Utc::now() - chrono::Duration::days(60)).to_rfc3339(); sqlx::query( "INSERT INTO dns_checks (target, name, record_type, expected, actual, matches, checked_at) VALUES (?, ?, ?, '[]', '[]', 1, ?)", ) .bind("mnw") .bind("makenot.work") .bind("A") .bind(&old_time) .execute(&pool) .await .unwrap(); // Insert recent DNS check let recent = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec![], actual: vec![], matches: true, checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; db::insert_dns_check(&pool, &recent).await.unwrap(); let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.dns, 1); let remaining = db::get_latest_dns_checks(&pool, "mnw").await.unwrap(); assert_eq!(remaining.len(), 1); } #[tokio::test] async fn prune_removes_old_whois_checks() { let pool = db::connect_in_memory().await.unwrap(); let old_time = (chrono::Utc::now() - chrono::Duration::days(60)).to_rfc3339(); sqlx::query( "INSERT INTO whois_checks (target, domain, checked_at) VALUES (?, ?, ?)", ) .bind("mnw") .bind("makenot.work") .bind(&old_time) .execute(&pool) .await .unwrap(); // Insert recent WHOIS check let recent = WhoisResult { target: "mnw".to_string(), domain: "makenot.work".to_string(), registrar: None, expiry_date: None, days_remaining: None, nameservers: vec![], checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; db::insert_whois_check(&pool, &recent).await.unwrap(); let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.whois, 1); let remaining = db::get_latest_whois_check(&pool, "mnw").await.unwrap(); assert!(remaining.is_some()); } // --- DNS/WHOIS API tests --- #[tokio::test] async fn api_status_includes_dns_status() { let pool = db::connect_in_memory().await.unwrap(); let config: pom::config::Config = toml::from_str(r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.health] url = "https://makenot.work/health" [[targets.mnw.dns]] name = "makenot.work" record_type = "A" expected = ["5.78.144.244"] "#).unwrap(); let app = pom::api::router(pool.clone(), config, None); // Insert DNS check data let dns_result = DnsCheckResult { target: "mnw".to_string(), name: "makenot.work".to_string(), record_type: pom::types::DnsRecordType::A, expected: vec!["5.78.144.244".to_string()], actual: vec!["5.78.144.244".to_string()], matches: true, checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; db::insert_dns_check(&pool, &dns_result).await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); let dns = json["dns_status"].as_array().unwrap(); assert_eq!(dns.len(), 1); assert_eq!(dns[0]["name"], "makenot.work"); assert_eq!(dns[0]["matches"], true); } #[tokio::test] async fn api_status_omits_empty_dns_status() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json.get("dns_status").is_none()); } #[tokio::test] async fn api_status_includes_whois() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool.clone(), config, None); let whois_result = WhoisResult { target: "mnw".to_string(), domain: "makenot.work".to_string(), registrar: Some("Namecheap, Inc.".to_string()), expiry_date: Some("2026-12-01T12:00:00Z".to_string()), days_remaining: Some(261), nameservers: vec!["ns1.example.com".to_string()], checked_at: chrono::Utc::now().to_rfc3339(), error: None, }; db::insert_whois_check(&pool, &whois_result).await.unwrap(); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json["whois"].is_object()); assert_eq!(json["whois"]["domain"], "makenot.work"); assert_eq!(json["whois"]["days_remaining"], 261); } #[tokio::test] async fn api_status_omits_whois_when_none() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); let app = pom::api::router(pool, config, None); let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json.get("whois").is_none()); } // --- DNS/WHOIS config parsing tests --- #[test] fn config_with_dns_records_parses() { let toml_str = r#" [targets.mnw] label = "MakeNotWork" [[targets.mnw.dns]] name = "makenot.work" record_type = "A" expected = ["5.78.144.244"] [[targets.mnw.dns]] name = "forums.makenot.work" record_type = "A" expected = ["5.78.144.244"] "#; let config: pom::config::Config = toml::from_str(toml_str).unwrap(); let mnw = config.get_target("mnw").unwrap(); assert_eq!(mnw.dns.len(), 2); assert_eq!(mnw.dns[0].name, "makenot.work"); assert_eq!(mnw.dns[0].record_type, pom::types::DnsRecordType::A); assert_eq!(mnw.dns[0].expected, vec!["5.78.144.244"]); assert_eq!(mnw.dns[1].name, "forums.makenot.work"); } #[test] fn config_with_whois_parses() { let toml_str = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.whois] domain = "makenot.work" warn_days = 60 "#; let config: pom::config::Config = toml::from_str(toml_str).unwrap(); let mnw = config.get_target("mnw").unwrap(); let whois = mnw.whois.as_ref().unwrap(); assert_eq!(whois.domain, "makenot.work"); assert_eq!(whois.warn_days, 60); } #[test] fn config_whois_default_warn_days() { let toml_str = r#" [targets.mnw] label = "MakeNotWork" [targets.mnw.whois] domain = "makenot.work" "#; let config: pom::config::Config = toml::from_str(toml_str).unwrap(); let whois = config.get_target("mnw").unwrap().whois.as_ref().unwrap(); assert_eq!(whois.warn_days, 30); } #[test] fn config_with_cors_parses() { let toml_str = r#" [targets.mnw] label = "MakeNotWork" [[targets.mnw.cors]] url = "https://example.com/bucket/probe" origin = "https://myapp.com" method = "PUT" [[targets.mnw.cors]] url = "https://example.com/bucket/probe2" origin = "https://myapp.com" "#; let config: pom::config::Config = toml::from_str(toml_str).unwrap(); let mnw = config.get_target("mnw").unwrap(); assert_eq!(mnw.cors.len(), 2); assert_eq!(mnw.cors[0].url, "https://example.com/bucket/probe"); assert_eq!(mnw.cors[0].origin, "https://myapp.com"); assert_eq!(mnw.cors[0].method, "PUT"); // Second entry uses default method assert_eq!(mnw.cors[1].method, "PUT"); } #[test] fn config_no_cors_defaults_to_empty() { let toml_str = r#" [targets.mnw] label = "MakeNotWork" "#; let config: pom::config::Config = toml::from_str(toml_str).unwrap(); let mnw = config.get_target("mnw").unwrap(); assert!(mnw.cors.is_empty()); } #[test] fn config_no_dns_defaults_to_empty() { let toml_str = r#" [targets.mnw] label = "MakeNotWork" "#; let config: pom::config::Config = toml::from_str(toml_str).unwrap(); let mnw = config.get_target("mnw").unwrap(); assert!(mnw.dns.is_empty()); assert!(mnw.whois.is_none()); } // --- Dashboard tests --- #[tokio::test] async fn dashboard_enabled_serves_html() { let pool = db::connect_in_memory().await.unwrap(); let mut config = test_config(); config.serve.dashboard = true; let app = pom::api::router(pool, config, None); let (status, body) = get_body(&app, "/").await; assert_eq!(status, 200); assert!(body.contains(""), "should contain doctype"); assert!(body.contains("PoM"), "should contain PoM title"); } #[tokio::test] async fn dashboard_disabled_returns_404() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config(); // dashboard defaults to false let app = pom::api::router(pool, config, None); let req = axum::http::Request::builder() .uri("/") .body(Body::empty()) .unwrap(); let resp = app.clone().oneshot(req).await.unwrap(); assert_eq!(resp.status().as_u16(), 404); } #[tokio::test] async fn dashboard_embeds_api_token() { let pool = db::connect_in_memory().await.unwrap(); let mut config = test_config(); config.serve.dashboard = true; config.serve.api_token = Some("test-secret-token-42".to_string()); let app = pom::api::router(pool, config, None); let (status, body) = get_body(&app, "/").await; assert_eq!(status, 200); assert!(body.contains("test-secret-token-42"), "should embed the API token"); } #[tokio::test] async fn dashboard_shows_mesh_section_when_mesh_enabled() { let pool = db::connect_in_memory().await.unwrap(); let mut config = test_config(); config.serve.dashboard = true; let mesh = test_mesh(); let app = pom::api::router(pool, config, Some(mesh)); let (status, body) = get_body(&app, "/").await; assert_eq!(status, 200); assert!(body.contains("Peer Mesh"), "should contain mesh section title"); assert!(body.contains("HAS_MESH = true"), "should set HAS_MESH to true"); } #[tokio::test] async fn dashboard_no_mesh_section_when_mesh_disabled() { let pool = db::connect_in_memory().await.unwrap(); let mut config = test_config(); config.serve.dashboard = true; let app = pom::api::router(pool, config, None); let (status, body) = get_body(&app, "/").await; assert_eq!(status, 200); assert!(body.contains("HAS_MESH = false"), "should set HAS_MESH to false"); } // --- Per-test detail tracking --- #[tokio::test] async fn insert_and_query_test_details() { let pool = db::connect_in_memory().await.unwrap(); let run = TestRun { id: None, target: "mnw".to_string(), started_at: "2026-03-16T00:00:00Z".to_string(), finished_at: Some("2026-03-16T00:02:00Z".to_string()), duration_secs: Some(120), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: Some(3), total_failed: Some(0), details: vec![ TestDetail { test_name: "foo::bar".to_string(), passed: true }, TestDetail { test_name: "foo::baz".to_string(), passed: true }, TestDetail { test_name: "foo::qux".to_string(), passed: true }, ], }, raw_output: String::new(), filter: None, }; let run_id = db::insert_test_run(&pool, &run).await.unwrap(); db::insert_test_details(&pool, run_id, &run.summary.details).await.unwrap(); // Verify via regression detection (no previous run = no regressions) let regressions = db::get_test_regressions(&pool, "mnw", run_id).await.unwrap(); assert!(regressions.is_empty()); } #[tokio::test] async fn regression_detection_finds_newly_failing_tests() { let pool = db::connect_in_memory().await.unwrap(); // First run: all pass let run1 = TestRun { id: None, target: "mnw".to_string(), started_at: "2026-03-16T00:00:00Z".to_string(), finished_at: Some("2026-03-16T00:02:00Z".to_string()), duration_secs: Some(120), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: Some(3), total_failed: Some(0), details: vec![ TestDetail { test_name: "foo::bar".to_string(), passed: true }, TestDetail { test_name: "foo::baz".to_string(), passed: true }, TestDetail { test_name: "foo::qux".to_string(), passed: true }, ], }, raw_output: String::new(), filter: None, }; let run1_id = db::insert_test_run(&pool, &run1).await.unwrap(); db::insert_test_details(&pool, run1_id, &run1.summary.details).await.unwrap(); // Second run: foo::baz fails let run2 = TestRun { id: None, target: "mnw".to_string(), started_at: "2026-03-16T00:05:00Z".to_string(), finished_at: Some("2026-03-16T00:07:00Z".to_string()), duration_secs: Some(120), exit_code: Some(1), passed: false, summary: TestSummary { steps: vec![], total_passed: Some(2), total_failed: Some(1), details: vec![ TestDetail { test_name: "foo::bar".to_string(), passed: true }, TestDetail { test_name: "foo::baz".to_string(), passed: false }, TestDetail { test_name: "foo::qux".to_string(), passed: true }, ], }, raw_output: String::new(), filter: None, }; let run2_id = db::insert_test_run(&pool, &run2).await.unwrap(); db::insert_test_details(&pool, run2_id, &run2.summary.details).await.unwrap(); let regressions = db::get_test_regressions(&pool, "mnw", run2_id).await.unwrap(); assert_eq!(regressions.len(), 1); assert_eq!(regressions[0], "foo::baz"); } #[tokio::test] async fn regression_ignores_already_failing_tests() { let pool = db::connect_in_memory().await.unwrap(); // Both runs: foo::baz fails for (i, ts) in ["00:00:00", "00:05:00"].iter().enumerate() { let run = TestRun { id: None, target: "mnw".to_string(), started_at: format!("2026-03-16T{ts}Z"), finished_at: None, duration_secs: Some(120), exit_code: Some(1), passed: false, summary: TestSummary { steps: vec![], total_passed: Some(2), total_failed: Some(1), details: vec![ TestDetail { test_name: "foo::bar".to_string(), passed: true }, TestDetail { test_name: "foo::baz".to_string(), passed: false }, ], }, raw_output: String::new(), filter: None, }; let run_id = db::insert_test_run(&pool, &run).await.unwrap(); db::insert_test_details(&pool, run_id, &run.summary.details).await.unwrap(); if i == 1 { // Second run — baz was already failing, not a regression let regressions = db::get_test_regressions(&pool, "mnw", run_id).await.unwrap(); assert!(regressions.is_empty()); } } } // --- Test duration drift detection --- #[tokio::test] async fn test_duration_drift_detected() { use pom::checks::http::detect_test_duration_drift; // 10 baseline runs at 60s, 3 recent runs at 120s (2x baseline > 1.5x threshold) let mut durations: Vec<(String, i64)> = Vec::new(); // Most recent first for i in 0..3 { durations.push((format!("2026-03-16T00:{:02}:00Z", 12 - i), 120)); } for i in 0..10 { durations.push((format!("2026-03-16T00:{:02}:00Z", 9 - i), 60)); } let drift = detect_test_duration_drift(&durations, 10, 3, 1.5); assert!(drift.is_some()); let msg = drift.unwrap(); assert!(msg.contains("drift"), "drift message: {msg}"); } #[tokio::test] async fn test_duration_no_drift_when_stable() { use pom::checks::http::detect_test_duration_drift; // All runs at ~60s let mut durations: Vec<(String, i64)> = Vec::new(); for i in 0..13 { durations.push((format!("2026-03-16T00:{:02}:00Z", 12 - i), 60)); } let drift = detect_test_duration_drift(&durations, 10, 3, 1.5); assert!(drift.is_none()); } #[tokio::test] async fn test_duration_drift_not_enough_data() { use pom::checks::http::detect_test_duration_drift; // Only 5 runs (need 13 for baseline 10 + recent 3) let durations: Vec<(String, i64)> = (0..5) .map(|i| (format!("2026-03-16T00:{:02}:00Z", i), 120)) .collect(); let drift = detect_test_duration_drift(&durations, 10, 3, 1.5); assert!(drift.is_none()); } #[tokio::test] async fn get_test_durations_returns_ordered() { let pool = db::connect_in_memory().await.unwrap(); for (i, secs) in [60, 80, 100].iter().enumerate() { let run = TestRun { id: None, target: "mnw".to_string(), started_at: format!("2026-03-16T00:{:02}:00Z", i), finished_at: None, duration_secs: Some(*secs), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: None, total_failed: None, details: vec![] }, raw_output: String::new(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); } let durations = db::get_test_durations(&pool, "mnw", 10).await.unwrap(); assert_eq!(durations.len(), 3); // Most recent first assert_eq!(durations[0].1, 100); assert_eq!(durations[2].1, 60); } // --- Uptime percent tests --- #[tokio::test] async fn uptime_percent_all_operational() { let pool = db::connect_in_memory().await.unwrap(); for i in 0..10 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::hours(i)).to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let pct = db::get_uptime_percent(&pool, "mnw", 24).await.unwrap(); assert_eq!(pct, Some(100.0)); } #[tokio::test] async fn uptime_percent_mixed() { let pool = db::connect_in_memory().await.unwrap(); // 8 operational for i in 0..8 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::hours(i)).to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } // 2 error for i in 8..10 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Error, checked_at: (chrono::Utc::now() - chrono::Duration::hours(i)).to_rfc3339(), response_time_ms: 0, details: None, error: Some("down".to_string()), }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } let pct = db::get_uptime_percent(&pool, "mnw", 24).await.unwrap(); assert!(pct.is_some()); let p = pct.unwrap(); assert!((p - 80.0).abs() < 0.01, "expected ~80.0, got {p}"); } #[tokio::test] async fn uptime_percent_no_data() { let pool = db::connect_in_memory().await.unwrap(); let pct = db::get_uptime_percent(&pool, "mnw", 24).await.unwrap(); assert_eq!(pct, None); } #[tokio::test] async fn uptime_percent_only_old_data() { let pool = db::connect_in_memory().await.unwrap(); // Insert checks from 48 hours ago for i in 0..5 { let snapshot = HealthSnapshot { id: None, target: "mnw".to_string(), status: HealthStatus::Operational, checked_at: (chrono::Utc::now() - chrono::Duration::hours(48 + i)).to_rfc3339(), response_time_ms: 100, details: None, error: None, }; db::insert_health_check(&pool, &snapshot).await.unwrap(); } // Query last 24h — should find nothing let pct = db::get_uptime_percent(&pool, "mnw", 24).await.unwrap(); assert_eq!(pct, None); } // --- Prune test_details orphan cleanup --- #[tokio::test] async fn prune_cascades_test_details_with_deleted_run() { let pool = db::connect_in_memory().await.unwrap(); // Insert an old test run with details let old_time = (chrono::Utc::now() - chrono::Duration::days(60)).to_rfc3339(); let run = TestRun { id: None, target: "mnw".to_string(), started_at: old_time, finished_at: None, duration_secs: Some(60), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: Some(2), total_failed: Some(0), details: vec![ TestDetail { test_name: "test_a".to_string(), passed: true }, TestDetail { test_name: "test_b".to_string(), passed: true }, ], }, raw_output: String::new(), filter: None, }; let run_id = db::insert_test_run(&pool, &run).await.unwrap(); db::insert_test_details(&pool, run_id, &run.summary.details).await.unwrap(); // Verify details exist let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM test_details WHERE run_id = ?") .bind(run_id.0) .fetch_one(&pool) .await .unwrap(); assert_eq!(count.0, 2); // Prune with 1-day retention — the old run gets deleted, CASCADE removes details let result = db::prune_old_records(&pool, 1).await.unwrap(); assert_eq!(result.tests, 1); // Verify details are gone (removed by ON DELETE CASCADE) let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM test_details WHERE run_id = ?") .bind(run_id.0) .fetch_one(&pool) .await .unwrap(); assert_eq!(count.0, 0); } #[tokio::test] async fn prune_cleans_up_orphaned_test_details() { let pool = db::connect_in_memory().await.unwrap(); // Create a real test run, add details, then delete the run directly to create orphans. // (FK constraints prevent inserting with a non-existent run_id.) let run = TestRun { id: None, target: "mnw".to_string(), started_at: chrono::Utc::now().to_rfc3339(), finished_at: None, duration_secs: Some(60), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: Some(1), total_failed: Some(0), details: vec![TestDetail { test_name: "orphan_test".to_string(), passed: true }], }, raw_output: String::new(), filter: None, }; let run_id = db::insert_test_run(&pool, &run).await.unwrap(); db::insert_test_details(&pool, run_id, &run.summary.details).await.unwrap(); // Disable FK enforcement temporarily to delete the run without cascading sqlx::query("PRAGMA foreign_keys = OFF").execute(&pool).await.unwrap(); sqlx::query("DELETE FROM test_runs WHERE id = ?") .bind(run_id.0) .execute(&pool) .await .unwrap(); sqlx::query("PRAGMA foreign_keys = ON").execute(&pool).await.unwrap(); // Verify orphan exists let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM test_details WHERE run_id = ?") .bind(run_id.0) .fetch_one(&pool) .await .unwrap(); assert_eq!(count.0, 1); // Prune — orphaned details should be cleaned up by the explicit orphan SQL let result = db::prune_old_records(&pool, 30).await.unwrap(); assert_eq!(result.test_details, 1); let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM test_details WHERE run_id = ?") .bind(run_id.0) .fetch_one(&pool) .await .unwrap(); assert_eq!(count.0, 0); } // --- API test_duration_drift field --- #[tokio::test] async fn api_status_target_includes_test_duration_drift() { let pool = db::connect_in_memory().await.unwrap(); let config = test_config_with_tests(); let app = pom::api::router(pool.clone(), config, None); // Insert 13 test runs: 10 baseline at 60s, 3 recent at 120s for i in 0..10 { let run = TestRun { id: None, target: "mnw".to_string(), started_at: format!("2026-03-16T{:02}:00:00Z", i), finished_at: None, duration_secs: Some(60), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: None, total_failed: None, details: vec![] }, raw_output: String::new(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); } for i in 10..13 { let run = TestRun { id: None, target: "mnw".to_string(), started_at: format!("2026-03-16T{:02}:00:00Z", i), finished_at: None, duration_secs: Some(120), exit_code: Some(0), passed: true, summary: TestSummary { steps: vec![], total_passed: None, total_failed: None, details: vec![] }, raw_output: String::new(), filter: None, }; db::insert_test_run(&pool, &run).await.unwrap(); } let (status, json) = api_get(&app, "/api/status/mnw").await; assert_eq!(status, 200); assert!(json["test_duration_drift"].is_string(), "expected test_duration_drift string, got: {}", json); let drift_msg = json["test_duration_drift"].as_str().unwrap(); assert!(drift_msg.contains("drift"), "drift message: {drift_msg}"); }