//! Listen-relevante Mutationen (Close, Reopen, Bulk-Close) müssen den //! pro-User Watermark bumpen. Sonst bleibt der ETag von //! `/api/oneliners` stehen, jeder Poll liefert 304, und der Client //! läuft dauerhaft mit einem Marker für einen Fall, den der Server //! bereits geschlossen hat (ursprünglicher Bugreport: "Polymyalgia //! rheumatica" blieb im Client sichtbar, obwohl im Web-UI bereits //! gelöscht). mod common; use std::path::Path; use axum::http::StatusCode; use doctate_common::BulkAction; use tower::util::ServiceExt; use common::{ TestConfig, csrf_form_post, get_with_api_key, get_with_api_key_if_none_match, header_opt, login_with_csrf, paths, test_admin, test_user, }; /// Create `///.m4a` so the case is /// visible to `enumerate_recent_oneliners` (window defaults to 72h; /// the fake mtime is "now" because write happens right now). fn seed_case_with_recording(data_path: &Path, slug: &str, case_id: &str) { let case_dir = common::seed_case(data_path, slug, case_id); std::fs::write(case_dir.join("2026-04-19T10-00-00Z.m4a"), b"fake audio").unwrap(); } async fn capture_etag( app: &axum::Router, api_key: &str, if_none_match: Option<&str>, ) -> (StatusCode, Option) { let req = match if_none_match { Some(tag) => get_with_api_key_if_none_match(paths::ONELINERS_PATH, api_key, tag), None => get_with_api_key(paths::ONELINERS_PATH, api_key), }; let resp = app.clone().oneshot(req).await.unwrap(); let status = resp.status(); let etag = header_opt(&resp, "etag").map(str::to_owned); (status, etag) } #[tokio::test] async fn close_bumps_watermark() { let cfg = TestConfig::new().with_user(test_user("dr_a")).build(); let case_id = "550e8400-e29b-41d4-a716-446655440000"; seed_case_with_recording(&cfg.data_path, "dr_a", case_id); let (app, store) = doctate_server::create_router_and_session_store(cfg); let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await; // First GET: capture baseline ETag (watermark empty → seeded on 0). let (s1, etag_before) = capture_etag(&app, "key-dr_a", None).await; assert_eq!(s1, StatusCode::OK); let etag_before = etag_before.expect("server must set ETag"); // Close via the web handler. let del = app .clone() .oneshot(csrf_form_post( paths::case_close(case_id), &cookie, &csrf, "", )) .await .unwrap(); assert_eq!( del.status().as_u16() / 100, 3, "close should redirect (3xx)" ); // Same If-None-Match now must NOT match — watermark was bumped. let (s2, etag_after) = capture_etag(&app, "key-dr_a", Some(&etag_before)).await; assert_eq!( s2, StatusCode::OK, "watermark bump must invalidate cached ETag", ); let etag_after = etag_after.expect("200 response must set ETag"); assert_ne!( etag_after, etag_before, "close handler must change the per-user ETag" ); } #[tokio::test] async fn reopen_bumps_watermark() { let cfg = TestConfig::new().with_user(test_user("dr_a")).build(); let case_id = "660e8400-e29b-41d4-a716-446655440000"; seed_case_with_recording(&cfg.data_path, "dr_a", case_id); let (app, store) = doctate_server::create_router_and_session_store(cfg); let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await; // Close first so reopen has something to restore; grab the post-close ETag. app.clone() .oneshot(csrf_form_post( paths::case_close(case_id), &cookie, &csrf, "", )) .await .unwrap(); let (_, etag_after_close) = capture_etag(&app, "key-dr_a", None).await; let etag_after_close = etag_after_close.expect("ETag must be set"); // Reopen. let reopen = app .clone() .oneshot(csrf_form_post( paths::case_reopen(case_id), &cookie, &csrf, "", )) .await .unwrap(); assert_eq!(reopen.status().as_u16() / 100, 3, "reopen should redirect"); // Reopen must bump again → post-close ETag becomes stale. let (s, etag_after_reopen) = capture_etag(&app, "key-dr_a", Some(&etag_after_close)).await; assert_eq!( s, StatusCode::OK, "reopen must change the ETag so clients re-fetch" ); let etag_after_reopen = etag_after_reopen.expect("200 response must set ETag"); assert_ne!(etag_after_reopen, etag_after_close); } #[tokio::test] async fn bulk_close_bumps_watermark() { // Bulk actions are admin-only: UI hides them from non-admins and // the handler rejects non-admin POSTs with 403. let cfg = TestConfig::new().with_user(test_admin("dr_a")).build(); let case_a = "770e8400-e29b-41d4-a716-446655440000"; let case_b = "880e8400-e29b-41d4-a716-446655440000"; seed_case_with_recording(&cfg.data_path, "dr_a", case_a); seed_case_with_recording(&cfg.data_path, "dr_a", case_b); let (app, store) = doctate_server::create_router_and_session_store(cfg); let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await; let (_, etag_before) = capture_etag(&app, "key-dr_a", None).await; let etag_before = etag_before.expect("ETag must be set"); // Form-encoded bulk close: action=close&case_id=a&case_id=b. let body = format!( "action={}&case_id={case_a}&case_id={case_b}", BulkAction::Close ); let bulk = app .clone() .oneshot(csrf_form_post(paths::BULK, &cookie, &csrf, &body)) .await .unwrap(); assert_eq!(bulk.status().as_u16() / 100, 3, "bulk should redirect"); let (s, etag_after) = capture_etag(&app, "key-dr_a", Some(&etag_before)).await; assert_eq!(s, StatusCode::OK, "bulk-close must bump the ETag"); let etag_after = etag_after.expect("200 response must set ETag"); assert_ne!(etag_after, etag_before); }