mod common; use axum::http::StatusCode; use tower::util::ServiceExt; use doctate_common::oneliners::OnelinerState; use common::{ DOCUMENT_FILE, TestConfig, body_string, get_with_cookie, paths, seed_case, seed_oneliner_ready, seed_oneliner_state, seed_recording, test_admin, test_user, }; /// Password "s" is the shared test default; wrap `login` so call sites /// don't repeat the literal. async fn login_dr_a(app: &axum::Router) -> String { common::login(app, "dr_a", "s").await } // --------------------------------------------------------------------- // GET /web/cases/{id} — case page // --------------------------------------------------------------------- #[tokio::test] async fn case_page_shows_document_when_present() { let cfg = TestConfig::new() .with_label("cp-doc") .with_user(test_user("dr_a")) .with_llm("http://unused") .build(); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); std::fs::write(case_dir.join(DOCUMENT_FILE), "der Inhalt").unwrap(); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = body_string(resp).await; assert!(body.contains("der Inhalt"), "document body missing"); assert!( body.contains(&paths::case_recordings(case_id)), "recordings sub-page link missing" ); } #[tokio::test] async fn case_page_document_has_copy_button() { let cfg = TestConfig::new() .with_label("cp-copy") .with_user(test_user("dr_a")) .with_llm("http://unused") .build(); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); std::fs::write(case_dir.join(DOCUMENT_FILE), "kopiere mich").unwrap(); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie)) .await .unwrap(); let body = body_string(resp).await; assert!(body.contains(r#"id="copy-btn""#), "copy button missing"); assert!( body.contains(r#"id="doc-content""#), "doc-content wrapper missing (JS depends on it)" ); } #[tokio::test] async fn case_page_shows_analyze_button_when_transcribed_without_document() { let (cfg, settings) = TestConfig::new() .with_label("cp-analyze") .with_user(test_user("dr_a")) .with_llm("http://unused") .build_pair(); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording( &case_dir, "2026-04-15T10-00-00Z", Some("transkribierter Text"), ); let app = doctate_server::create_router_with_settings(cfg, settings); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie)) .await .unwrap(); let body = body_string(resp).await; assert!( body.contains(&format!(r#"action="{}""#, paths::case_analyze(case_id))), "analyze form missing" ); assert!( !body.contains(r#"id="doc-content""#), "doc-content must not appear without a document" ); } #[tokio::test] async fn case_page_shows_llm_missing_hint_without_llm() { let cfg = TestConfig::new() .with_label("cp-llm") .with_user(test_user("dr_a")) .build(); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording(&case_dir, "2026-04-15T10-00-00Z", Some("Text")); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie)) .await .unwrap(); let body = body_string(resp).await; assert!( body.contains("LLM-Analyse nicht konfiguriert"), "expected llm-missing hint" ); } #[tokio::test] async fn case_page_empty_case_shows_placeholder() { let cfg = TestConfig::new() .with_label("cp-empty") .with_user(test_user("dr_a")) .with_llm("http://unused") .build(); let case_id = "11111111-1111-1111-1111-111111111111"; seed_case(&cfg.data_path, "dr_a", case_id); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = body_string(resp).await; assert!( body.contains("Noch keine Aufnahmen vorhanden"), "empty-case placeholder missing" ); assert!( body.contains(&format!(r#"action="{}""#, paths::case_close(case_id))), "close form missing on empty case" ); } #[tokio::test] async fn case_page_foreign_user_returns_404() { let cfg = TestConfig::new() .with_label("cp-idor") .with_user(test_user("dr_a")) .with_user(test_user("dr_b")) .with_llm("http://unused") .build(); let case_id = "22222222-2222-2222-2222-222222222222"; // Case exists under dr_b, but session is dr_a → 404. let case_dir = seed_case(&cfg.data_path, "dr_b", case_id); std::fs::write(case_dir.join(DOCUMENT_FILE), "fremdes Dokument").unwrap(); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } // --------------------------------------------------------------------- // GET /web/cases/{id}/recordings — recordings sub-page // --------------------------------------------------------------------- #[tokio::test] async fn case_recordings_shows_all_recordings() { let cfg = TestConfig::new() .with_label("rec-list") .with_user(test_user("dr_a")) .with_llm("http://unused") .build(); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording(&case_dir, "2026-04-15T10-00-00Z", Some("erste Aufnahme")); seed_recording(&case_dir, "2026-04-15T11-00-00Z", None); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_recordings(case_id), &cookie)) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let body = body_string(resp).await; assert!(body.contains("2026-04-15T10-00-00Z.m4a")); assert!(body.contains("2026-04-15T11-00-00Z.m4a")); assert!(body.contains("erste Aufnahme")); assert!(body.contains("Transkription ausstehend")); } #[tokio::test] async fn case_recordings_back_link_targets_case_page() { let cfg = TestConfig::new() .with_label("rec-back") .with_user(test_user("dr_a")) .with_llm("http://unused") .build(); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording(&case_dir, "2026-04-15T10-00-00Z", None); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_recordings(case_id), &cookie)) .await .unwrap(); let body = body_string(resp).await; assert!( body.contains(&format!(r#"href="{}""#, paths::case_detail(case_id))), "back link must target the case page, not /web/cases" ); } #[tokio::test] async fn case_page_uses_oneliner_as_h1_when_present() { let cfg = TestConfig::new() .with_label("cp-h1") .with_user(test_user("dr_a")) .with_llm("http://unused") .build(); let case_id = "11111111-1111-1111-1111-111111111111"; let case_dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_oneliner_ready(&case_dir, "Herzkatheter unauffällig"); std::fs::write(case_dir.join(DOCUMENT_FILE), "Inhalt").unwrap(); let app = doctate_server::create_router(cfg); let cookie = login_dr_a(&app).await; let resp = app .oneshot(get_with_cookie(paths::case_detail(case_id), &cookie)) .await .unwrap(); let body = body_string(resp).await; // H1 wraps the oneliner; the short case_id fallback must be gone. assert!( body.contains("