//! Integration tests for the admin overview aggregates endpoint //! (`GET /api/v1/admin/overview`). Auth gating mirrors the system tab; //! the aggregate counts are asserted against seeded data. mod common; use axum::http::StatusCode; use axum::Router; use serde_json::json; 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 } /// Seed a manga → chapter with `n` pages; mark the first `analyzed` of /// them as a `done` page_analysis. Returns the page ids. async fn seed_manga_pages(pool: &PgPool, title: &str, n: i32, analyzed: i32) -> Vec { let manga_id: uuid::Uuid = sqlx::query_scalar("INSERT INTO mangas (title) VALUES ($1) RETURNING id") .bind(title) .fetch_one(pool) .await .unwrap(); let chapter_id: uuid::Uuid = sqlx::query_scalar("INSERT INTO chapters (manga_id, number) VALUES ($1, 1) RETURNING id") .bind(manga_id) .fetch_one(pool) .await .unwrap(); let mut ids = Vec::new(); for i in 1..=n { let id: uuid::Uuid = sqlx::query_scalar( "INSERT INTO pages (chapter_id, page_number, storage_key, content_type) \ VALUES ($1, $2, $3, 'image/png') RETURNING id", ) .bind(chapter_id) .bind(i) .bind(format!("k/{manga_id}/{i}.png")) .fetch_one(pool) .await .unwrap(); if i <= analyzed { sqlx::query("INSERT INTO page_analysis (page_id, status) VALUES ($1, 'done')") .bind(id) .execute(pool) .await .unwrap(); } ids.push(id); } ids } #[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/overview", &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/overview")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "./migrations")] async fn aggregates_users_mangas_and_coverage(pool: PgPool) { let h = common::harness(pool.clone()); // seed_admin registers one user and promotes it; register a second // (non-admin) user so the counts have a known split. let cookie = seed_admin(&pool, &h.app).await; let _ = common::register_user(&h.app).await; // Two mangas: 3 pages with 2 analyzed, and 2 pages with 0 analyzed. seed_manga_pages(&pool, "Alpha", 3, 2).await; seed_manga_pages(&pool, "Beta", 2, 0).await; let resp = h .app .oneshot(common::get_with_cookie("/api/v1/admin/overview", &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = common::body_json(resp).await; let users = body.get("users").expect("users key"); assert_eq!(users["total"], json!(2)); assert_eq!(users["admins"], json!(1)); assert!(users["newest_username"].is_string()); let mangas = body.get("mangas").expect("mangas key"); assert_eq!(mangas["total"], json!(2)); // Uploaded mangas (no manga_sources) classify as synced. assert_eq!(mangas["synced"], json!(2)); assert_eq!(mangas["in_progress"], json!(0)); assert_eq!(mangas["dropped"], json!(0)); assert_eq!(mangas["total_chapters"], json!(2)); assert_eq!(mangas["total_pages"], json!(5)); assert!(mangas["newest_title"].is_string()); let analysis = body.get("analysis").expect("analysis key"); assert_eq!(analysis["total_pages"], json!(5)); assert_eq!(analysis["analyzed_pages"], json!(2)); }