//! PR 4 (feat/admin-system-api) integration tests. //! //! Shape-only assertions — we don't mock the system, just call the //! endpoint and check the response envelope. Threshold-triggering of //! alerts would require faking statvfs / sysinfo, which is more //! plumbing than the test gives back. mod common; use axum::http::StatusCode; use axum::Router; use sqlx::PgPool; use tower::ServiceExt; use mangalord::repo; async fn seed_admin(pool: &PgPool, app: &Router) -> String { let (username, cookie) = common::register_user(app).await; let u = repo::user::find_by_username(pool, &username) .await .unwrap() .unwrap(); repo::user::set_is_admin_unchecked(pool, u.id, true).await.unwrap(); cookie } #[sqlx::test(migrations = "./migrations")] async fn requires_admin(pool: PgPool) { let h = common::harness(pool); let (_u, cookie) = common::register_user(&h.app).await; let resp = h .app .oneshot(common::get_with_cookie("/api/v1/admin/system", &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); } #[sqlx::test(migrations = "./migrations")] async fn unauthenticated_request_is_rejected(pool: PgPool) { let h = common::harness(pool); let resp = h .app .oneshot(common::get("/api/v1/admin/system")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "./migrations")] async fn returns_disk_memory_cpu_alerts_shape(pool: PgPool) { let h = common::harness(pool.clone()); let cookie = seed_admin(&pool, &h.app).await; let resp = h .app .oneshot(common::get_with_cookie("/api/v1/admin/system", &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = common::body_json(resp).await; // Disk: harness uses LocalStorage on a tempdir, so disk SHOULD be // populated. Validate the field shape and percent range. let disk = body .get("disk") .expect("disk key present") .as_object() .expect("disk is an object (LocalStorage exposes a path)"); assert!(disk["total_bytes"].as_u64().unwrap() > 0); let pct = disk["percent_used"].as_f64().unwrap(); assert!( (0.0..=100.0).contains(&pct), "percent_used outside [0,100]: {pct}" ); let mem = body.get("memory").expect("memory key").as_object().unwrap(); assert!(mem["total_bytes"].as_u64().unwrap() > 0); let mpct = mem["percent_used"].as_f64().unwrap(); assert!((0.0..=100.0).contains(&mpct)); let cpu = body.get("cpu").expect("cpu key").as_object().unwrap(); let cpu_pct = cpu["percent_used"].as_f64().unwrap(); assert!( (0.0..=100.0).contains(&cpu_pct), "cpu out of range: {cpu_pct}" ); // Load average: three numeric keys. Values can legitimately be 0 on // an idle box, so only assert presence + non-negativity. let load = cpu["load_avg"].as_object().expect("cpu.load_avg object"); for k in ["one", "five", "fifteen"] { let v = load[k].as_f64().unwrap_or_else(|| panic!("load_avg.{k} numeric")); assert!(v >= 0.0, "load_avg.{k} negative: {v}"); } // Per-core usage: an array of percentages in [0,100]. let cores = cpu["per_core"].as_array().expect("cpu.per_core array"); assert!(!cores.is_empty(), "expected at least one core"); for c in cores { let p = c.as_f64().unwrap(); assert!((0.0..=100.0).contains(&p), "per_core out of range: {p}"); } // Temperatures: an array (possibly empty in CI / containers without // /sys sensors). Each present entry has a string label + numeric °C. let temps = body .get("temperatures") .expect("temperatures key") .as_array() .expect("temperatures is an array"); for t in temps { assert!(t["label"].is_string(), "temp label is string"); assert!(t["celsius"].as_f64().is_some(), "temp celsius is numeric"); } let alerts = body.get("alerts").expect("alerts key").as_array().unwrap(); // Don't assert on length — the box may genuinely be >90% on memory // when the test runs. Just confirm shape of any present entry. for alert in alerts { assert!(alert["level"].is_string()); assert!(alert["message"].is_string()); } }