//! Site-wide auth gate (`PRIVATE_MODE=true`). //! //! With private mode on, every API path except a small allowlist //! (`/health`, `/auth/config`, `/auth/login`, `/auth/logout`) requires //! a valid session cookie or bearer token, and `/auth/register` is //! force-blocked regardless of `ALLOW_SELF_REGISTER`. With private mode //! off (the default), nothing changes — the `public_mode_*` test //! pins that regression guard. mod common; use serde_json::json; use sqlx::PgPool; use tower::ServiceExt; use axum::http::StatusCode; #[sqlx::test(migrations = "./migrations")] async fn private_mode_blocks_anonymous_manga_list(pool: PgPool) { let h = common::harness_with_private_mode(pool); let resp = h.app.oneshot(common::get("/api/v1/mangas")).await.unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "./migrations")] async fn private_mode_blocks_anonymous_files(pool: PgPool) { let h = common::harness_with_private_mode(pool); // The path doesn't have to exist — the guard runs before routing, // so the response is 401 (not 404). That's the property the test // is pinning: nothing leaks via crafted URLs. let resp = h .app .oneshot(common::get("/api/v1/files/anything.png")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[sqlx::test(migrations = "./migrations")] async fn private_mode_allows_session_cookie_read(pool: PgPool) { // Register through a non-private harness sharing the same DB pool // so the session row exists. Then exercise the gate using a fresh // private-mode harness against the same DB. let public = common::harness(pool.clone()); let (_, cookie) = common::register_user(&public.app).await; let private = common::harness_with_private_mode(pool); let resp = private .app .oneshot(common::get_with_cookie("/api/v1/mangas", &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[sqlx::test(migrations = "./migrations")] async fn private_mode_allows_bearer_token_read(pool: PgPool) { let public = common::harness(pool.clone()); let (_, cookie) = common::register_user(&public.app).await; let resp = public .app .clone() .oneshot(common::post_json_with_cookie( "/api/v1/auth/tokens", json!({ "name": "private-mode-bot" }), &cookie, )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); let body = common::body_json(resp).await; let bearer = body["bearer"].as_str().unwrap().to_string(); let private = common::harness_with_private_mode(pool); let resp = private .app .oneshot(common::get_with_bearer("/api/v1/mangas", &bearer)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[sqlx::test(migrations = "./migrations")] async fn private_mode_allows_login_endpoint_anonymous(pool: PgPool) { // Seed a user via the public harness so login has credentials to // verify against. let public = common::harness(pool.clone()); let _ = public .app .clone() .oneshot(common::post_json( "/api/v1/auth/register", json!({ "username": "alice", "password": "hunter2hunter2" }), )) .await .unwrap(); let private = common::harness_with_private_mode(pool); let resp = private .app .oneshot(common::post_json( "/api/v1/auth/login", json!({ "username": "alice", "password": "hunter2hunter2" }), )) .await .unwrap(); // Reaches the login handler and succeeds — *not* 401 from the // gate. That's the property we're pinning. assert_eq!(resp.status(), StatusCode::OK); } #[sqlx::test(migrations = "./migrations")] async fn private_mode_allows_health_and_config_anonymous(pool: PgPool) { let h = common::harness_with_private_mode(pool); let r = h .app .clone() .oneshot(common::get("/api/v1/health")) .await .unwrap(); assert_eq!(r.status(), StatusCode::OK); let r = h .app .oneshot(common::get("/api/v1/auth/config")) .await .unwrap(); assert_eq!(r.status(), StatusCode::OK); } #[sqlx::test(migrations = "./migrations")] async fn private_mode_blocks_register_even_when_self_register_enabled(pool: PgPool) { // harness_with_private_mode keeps `allow_self_register=true` (the // default) — private mode is supposed to force-block register // regardless. That's what this test pins. let h = common::harness_with_private_mode(pool); let resp = h .app .oneshot(common::post_json( "/api/v1/auth/register", json!({ "username": "alice", "password": "hunter2hunter2" }), )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); let body = common::body_json(resp).await; assert_eq!(body["error"]["code"], "forbidden"); } #[sqlx::test(migrations = "./migrations")] async fn auth_config_reports_private_mode_and_effective_self_register(pool: PgPool) { let h = common::harness_with_private_mode(pool); let resp = h .app .oneshot(common::get("/api/v1/auth/config")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = common::body_json(resp).await; assert_eq!(body["private_mode"], true); // Effective value: `allow_self_register && !private_mode` is false // here even though the raw `allow_self_register` is true. assert_eq!(body["self_register_enabled"], false); } #[sqlx::test(migrations = "./migrations")] async fn public_mode_does_not_gate_anonymous_reads(pool: PgPool) { // Regression guard: with private_mode off (the default), the gate // must be a no-op so existing public deployments stay public. let h = common::harness(pool); let resp = h.app.oneshot(common::get("/api/v1/mangas")).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[sqlx::test(migrations = "./migrations")] async fn public_mode_reports_private_mode_false(pool: PgPool) { let h = common::harness(pool); let resp = h .app .oneshot(common::get("/api/v1/auth/config")) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = common::body_json(resp).await; assert_eq!(body["private_mode"], false); assert_eq!(body["self_register_enabled"], true); }