//! Lazy retention sweep: GET /cases must auto-close stale cases //! and auto-purge old closed cases, driven by per-user retention //! settings from users.toml. Tests age the *comparison objects* //! (m4a mtime, closed_at string) instead of faking "now" — gives //! realistic behavioural coverage without a clock abstraction. mod common; use std::time::Duration; use axum::http::StatusCode; use tower::util::ServiceExt; use common::{ CLOSE_MARKER, TestConfig, get_with_cookie, past_rfc3339, paths, seed_case, seed_recording_with_age, test_user_with_retention, write_closed_marker, }; /// Trigger the sweep via its natural path: GET /cases. async fn trigger_sweep(app: &axum::Router, cookie: &str) { let resp = app .clone() .oneshot(get_with_cookie(paths::CASES, cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); } fn build_cfg( label: &'static str, auto_close_days: u32, auto_delete_days: u32, ) -> std::sync::Arc { TestConfig::new() .with_label(label) .with_user(test_user_with_retention( "dr_a", auto_close_days, auto_delete_days, )) .build() } #[tokio::test] async fn auto_close_fires_when_last_recording_too_old() { let cfg = build_cfg("close-old", 7, 0); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording_with_age( &case_dir, "2026-04-01T10-00-00Z", Duration::from_secs(8 * 86400), ); let app = doctate_server::create_router(cfg); let cookie = common::login(&app, "dr_a", "s").await; assert!(!case_dir.join(CLOSE_MARKER).exists()); trigger_sweep(&app, &cookie).await; assert!( case_dir.join(CLOSE_MARKER).exists(), "stale case must be auto-closed" ); } #[tokio::test] async fn auto_close_disabled_when_days_zero() { // auto_close_days = 0 → sweep never closes for age; even a very // old recording must stay open. let cfg = build_cfg("close-zero", 0, 0); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording_with_age( &case_dir, "2026-04-01T10-00-00Z", Duration::from_secs(365 * 86400), ); let app = doctate_server::create_router(cfg); let cookie = common::login(&app, "dr_a", "s").await; trigger_sweep(&app, &cookie).await; assert!( !case_dir.join(CLOSE_MARKER).exists(), "case must stay open when auto_close_days = 0" ); } #[tokio::test] async fn auto_close_respects_newest_recording() { // Case has one very old .m4a and one fresh .m4a — must stay open. let cfg = build_cfg("close-newest", 7, 0); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording_with_age( &case_dir, "2026-04-01T10-00-00Z", Duration::from_secs(30 * 86400), ); seed_recording_with_age(&case_dir, "2026-04-01T11-00-00Z", Duration::from_secs(60)); let app = doctate_server::create_router(cfg); let cookie = common::login(&app, "dr_a", "s").await; trigger_sweep(&app, &cookie).await; assert!( !case_dir.join(CLOSE_MARKER).exists(), "recent addendum must keep the case open" ); } #[tokio::test] async fn auto_purge_removes_old_closed_case() { let cfg = build_cfg("purge-old", 0, 30); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording_with_age(&case_dir, "2026-04-01T10-00-00Z", Duration::from_secs(60)); // closed_at = 31 days ago → must be purged. write_closed_marker(&case_dir, &past_rfc3339(Duration::from_secs(31 * 86400))); let app = doctate_server::create_router(cfg); let cookie = common::login(&app, "dr_a", "s").await; trigger_sweep(&app, &cookie).await; assert!( !case_dir.exists(), "old closed case must be purged (directory gone)" ); } #[tokio::test] async fn auto_purge_disabled_when_days_zero() { // Admin-test guarantee: auto_delete_days = 0 means closed cases // are retained indefinitely. let cfg = build_cfg("purge-zero", 0, 0); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording_with_age(&case_dir, "2026-04-01T10-00-00Z", Duration::from_secs(60)); write_closed_marker(&case_dir, &past_rfc3339(Duration::from_secs(365 * 86400))); let app = doctate_server::create_router(cfg); let cookie = common::login(&app, "dr_a", "s").await; trigger_sweep(&app, &cookie).await; assert!( case_dir.exists(), "closed case must survive when auto_delete_days = 0" ); assert!(case_dir.join(CLOSE_MARKER).exists()); } #[tokio::test] async fn fresh_auto_close_is_not_immediately_purged() { // Vacation scenario: user returns after 40 days offline. // auto_close_days = 7, auto_delete_days = 30. // The sweep auto-closes (step 1, closed_at ≈ now), but the purge // check (step 2) sees closed_at ≈ now and skips — the user gets // the full 30-day grace period, nothing disappears silently. let cfg = build_cfg("vacation", 7, 30); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording_with_age( &case_dir, "2026-04-01T10-00-00Z", Duration::from_secs(40 * 86400), ); let app = doctate_server::create_router(cfg); let cookie = common::login(&app, "dr_a", "s").await; trigger_sweep(&app, &cookie).await; assert!( case_dir.exists(), "vacation: case directory must NOT be purged in the same sweep that auto-closed it" ); assert!( case_dir.join(CLOSE_MARKER).exists(), "vacation: case must be auto-closed (but retained)" ); } #[tokio::test] async fn auto_purge_skips_open_cases() { let cfg = build_cfg("purge-skip-open", 0, 30); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording_with_age(&case_dir, "2026-04-01T10-00-00Z", Duration::from_secs(60)); let app = doctate_server::create_router(cfg); let cookie = common::login(&app, "dr_a", "s").await; trigger_sweep(&app, &cookie).await; assert!( case_dir.exists(), "open case without .closed marker must never be purged" ); }