Formatting
This commit is contained in:
+101
-39
@@ -4,10 +4,10 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use doctate_server::analyze;
|
||||
use doctate_server::config::{Config, User};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use tower::util::ServiceExt;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
@@ -165,7 +165,10 @@ async fn analyze_foreign_case_returns_404() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request(foreign_case, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request(foreign_case, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -175,7 +178,10 @@ async fn analyze_invalid_uuid_returns_400() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request("not-a-uuid", &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request("not-a-uuid", &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
@@ -189,7 +195,10 @@ async fn analyze_case_without_recordings_returns_400() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
@@ -204,7 +213,10 @@ async fn analyze_case_with_missing_transcript_returns_400() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
@@ -218,7 +230,10 @@ async fn analyze_case_without_llm_returns_503() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@@ -232,15 +247,26 @@ async fn analyze_case_happy_path_writes_input_and_redirects() {
|
||||
let case_id = "11111111-1111-1111-1111-111111111111";
|
||||
let case_dir = seed_case(&config.data_path, "dr_a", case_id);
|
||||
seed_recording(&case_dir, "10-00-00", Some("Patient klagt über Knie."));
|
||||
seed_recording(&case_dir, "10-05-00", Some("Korrektur: links, nicht rechts."));
|
||||
seed_recording(
|
||||
&case_dir,
|
||||
"10-05-00",
|
||||
Some("Korrektur: links, nicht rechts."),
|
||||
);
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::LOCATION).unwrap().to_str().unwrap(),
|
||||
resp.headers()
|
||||
.get(header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"/web/cases"
|
||||
);
|
||||
|
||||
@@ -267,10 +293,17 @@ async fn analyze_case_second_time_returns_409() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let first = app.clone().oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let first = app
|
||||
.clone()
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(first.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
let second = app.oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let second = app
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(second.status(), StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
@@ -286,7 +319,10 @@ async fn analyze_case_skips_blank_transcript_from_recordings() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
let raw = std::fs::read_to_string(case_dir.join("analysis_input.json")).unwrap();
|
||||
@@ -515,7 +551,10 @@ async fn recovery_skips_completed_analysis() {
|
||||
let (tx, mut rx) = analyze::channel();
|
||||
analyze::recovery::scan_and_enqueue(&tmp, &tx).await;
|
||||
|
||||
assert!(rx.try_recv().is_err(), "completed analysis must not re-enqueue");
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"completed analysis must not re-enqueue"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -543,7 +582,9 @@ async fn document_view_reads_document() {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
|
||||
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let body_str = String::from_utf8(body.to_vec()).unwrap();
|
||||
assert!(body_str.contains("der Inhalt"), "expected content in body");
|
||||
assert!(body_str.contains("Dokument"));
|
||||
@@ -565,7 +606,10 @@ async fn analyze_deletes_old_document_and_writes_fresh_input() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.oneshot(analyze_request(case_id, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(analyze_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
assert!(
|
||||
@@ -574,11 +618,11 @@ async fn analyze_deletes_old_document_and_writes_fresh_input() {
|
||||
);
|
||||
let input_path = case_dir.join("analysis_input.json");
|
||||
assert!(input_path.exists(), "analysis_input.json missing");
|
||||
let parsed: Value = serde_json::from_str(&std::fs::read_to_string(&input_path).unwrap()).unwrap();
|
||||
let parsed: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(&input_path).unwrap()).unwrap();
|
||||
assert_eq!(parsed["recordings"].as_array().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Soft-delete + Undo
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -596,7 +640,11 @@ async fn delete_writes_marker_and_redirects() {
|
||||
let resp = app.oneshot(delete_request(case_id, &cookie)).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::LOCATION).unwrap().to_str().unwrap(),
|
||||
resp.headers()
|
||||
.get(header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"/web/cases"
|
||||
);
|
||||
let marker = case_dir.join(".deleted");
|
||||
@@ -604,7 +652,10 @@ async fn delete_writes_marker_and_redirects() {
|
||||
let raw = std::fs::read_to_string(&marker).unwrap();
|
||||
let parsed: Value = serde_json::from_str(&raw).unwrap();
|
||||
assert!(parsed["batch"].is_string(), "batch must be a UUID string");
|
||||
assert!(parsed["deleted_at"].is_string(), "deleted_at must be a string");
|
||||
assert!(
|
||||
parsed["deleted_at"].is_string(),
|
||||
"deleted_at must be a string"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -617,7 +668,11 @@ async fn deleted_case_returns_404_on_detail() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app.clone().oneshot(delete_request(case_id, &cookie)).await.unwrap();
|
||||
let resp = app
|
||||
.clone()
|
||||
.oneshot(delete_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
let resp = app
|
||||
@@ -647,12 +702,20 @@ async fn undo_delete_restores_latest_batch_only() {
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
// Two separate delete clicks → two distinct batches.
|
||||
let _ = app.clone().oneshot(delete_request(case_a, &cookie)).await.unwrap();
|
||||
let _ = app
|
||||
.clone()
|
||||
.oneshot(delete_request(case_a, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
// Batch IDs derive from `now_rfc3339()`, which rounds to whole
|
||||
// seconds — so the sleep must cross a second boundary for the two
|
||||
// deletes to land in distinct batches.
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
let _ = app.clone().oneshot(delete_request(case_b, &cookie)).await.unwrap();
|
||||
let _ = app
|
||||
.clone()
|
||||
.oneshot(delete_request(case_b, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let undo = app
|
||||
.clone()
|
||||
@@ -669,7 +732,10 @@ async fn undo_delete_restores_latest_batch_only() {
|
||||
assert_eq!(undo.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
// case_b (latest delete) restored, case_a still deleted.
|
||||
assert!(!dir_b.join(".deleted").exists(), "case_b marker should be gone");
|
||||
assert!(
|
||||
!dir_b.join(".deleted").exists(),
|
||||
"case_b marker should be gone"
|
||||
);
|
||||
assert!(dir_a.join(".deleted").exists(), "case_a marker must remain");
|
||||
}
|
||||
|
||||
@@ -701,9 +767,14 @@ async fn bulk_delete_shares_one_batch_uuid() {
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
let m_a: Value = serde_json::from_str(&std::fs::read_to_string(dir_a.join(".deleted")).unwrap()).unwrap();
|
||||
let m_b: Value = serde_json::from_str(&std::fs::read_to_string(dir_b.join(".deleted")).unwrap()).unwrap();
|
||||
assert_eq!(m_a["batch"], m_b["batch"], "bulk-delete must share batch UUID");
|
||||
let m_a: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(dir_a.join(".deleted")).unwrap()).unwrap();
|
||||
let m_b: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(dir_b.join(".deleted")).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
m_a["batch"], m_b["batch"],
|
||||
"bulk-delete must share batch UUID"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -814,15 +885,9 @@ async fn reset_case_clears_derived_and_unfails_audio() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app
|
||||
.oneshot(reset_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = app.oneshot(reset_request(case_id, &cookie)).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::LOCATION).unwrap(),
|
||||
"/web/cases"
|
||||
);
|
||||
assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/web/cases");
|
||||
|
||||
// Audio preserved, .failed renamed to .m4a.
|
||||
assert!(dir.join("2026-04-15T09-00-00Z.m4a").exists());
|
||||
@@ -848,10 +913,7 @@ async fn reset_case_requires_admin() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
let resp = app
|
||||
.oneshot(reset_request(case_id, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = app.oneshot(reset_request(case_id, &cookie)).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
// Nothing touched.
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use doctate_common::constants::API_KEY_HEADER;
|
||||
use doctate_common::oneliners::ONELINERS_PATH;
|
||||
use doctate_server::config::{Config, User};
|
||||
@@ -32,8 +32,10 @@ fn test_config_with_users(users: Vec<User>) -> Arc<Config> {
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let api_keys: HashMap<String, String> =
|
||||
users.iter().map(|u| (u.api_key.clone(), u.slug.clone())).collect();
|
||||
let api_keys: HashMap<String, String> = users
|
||||
.iter()
|
||||
.map(|u| (u.api_key.clone(), u.slug.clone()))
|
||||
.collect();
|
||||
Arc::new(Config {
|
||||
data_path,
|
||||
users,
|
||||
@@ -112,7 +114,11 @@ async fn delete_bumps_watermark() {
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let login = app.clone().oneshot(login_request("dr_a", "s")).await.unwrap();
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).expect("login should set cookie");
|
||||
|
||||
// First GET: capture baseline ETag (watermark empty → seeded on 0).
|
||||
@@ -162,7 +168,11 @@ async fn undo_delete_bumps_watermark() {
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let login = app.clone().oneshot(login_request("dr_a", "s")).await.unwrap();
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).expect("login should set cookie");
|
||||
|
||||
// Delete first so undo has something to restore; grab the post-delete ETag.
|
||||
@@ -217,7 +227,11 @@ async fn bulk_delete_bumps_watermark() {
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let login = app.clone().oneshot(login_request("dr_a", "s")).await.unwrap();
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).expect("login should set cookie");
|
||||
|
||||
let (_, etag_before) = capture_etag(&app, "key-dr_a", None).await;
|
||||
@@ -232,10 +246,7 @@ async fn bulk_delete_bumps_watermark() {
|
||||
.method("POST")
|
||||
.uri("/web/cases/bulk")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
.body(Body::from(form))
|
||||
.unwrap(),
|
||||
)
|
||||
|
||||
+62
-23
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
use doctate_server::config::{Config, User};
|
||||
@@ -23,8 +23,10 @@ fn test_config_with_users(users: Vec<User>) -> Arc<Config> {
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let api_keys: HashMap<String, String> =
|
||||
users.iter().map(|u| (u.api_key.clone(), u.slug.clone())).collect();
|
||||
let api_keys: HashMap<String, String> = users
|
||||
.iter()
|
||||
.map(|u| (u.api_key.clone(), u.slug.clone()))
|
||||
.collect();
|
||||
Arc::new(Config {
|
||||
data_path,
|
||||
users,
|
||||
@@ -69,14 +71,27 @@ async fn login_success_sets_session_cookie_and_redirects() {
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let resp = app.oneshot(login_request("dr_a", "secret")).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER.as_u16(), "got {:?}", resp.status());
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
StatusCode::SEE_OTHER.as_u16(),
|
||||
"got {:?}",
|
||||
resp.status()
|
||||
);
|
||||
|
||||
let loc = resp.headers().get(header::LOCATION).unwrap().to_str().unwrap();
|
||||
let loc = resp
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
assert_eq!(loc, "/web/cases");
|
||||
|
||||
let cookie = extract_session_cookie(&resp).expect("no session cookie");
|
||||
assert!(cookie.starts_with("session="));
|
||||
assert!(cookie.len() > "session=".len() + 20, "token looks too short: {cookie}");
|
||||
assert!(
|
||||
cookie.len() > "session=".len() + 20,
|
||||
"token looks too short: {cookie}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -96,7 +111,10 @@ async fn login_unknown_user_shows_error() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "secret")]);
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let resp = app.oneshot(login_request("ghost", "anything")).await.unwrap();
|
||||
let resp = app
|
||||
.oneshot(login_request("ghost", "anything"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert!(extract_session_cookie(&resp).is_none());
|
||||
}
|
||||
@@ -107,25 +125,35 @@ async fn cases_without_cookie_redirects_to_login() {
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let resp = app
|
||||
.oneshot(Request::builder().uri("/web/cases").body(Body::empty()).unwrap())
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/web/cases")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::LOCATION).unwrap().to_str().unwrap(),
|
||||
resp.headers()
|
||||
.get(header::LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap(),
|
||||
"/web/login"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cases_with_valid_cookie_shows_only_own_cases() {
|
||||
let config = test_config_with_users(vec![
|
||||
make_user("dr_a", "s"),
|
||||
make_user("dr_b", "s"),
|
||||
]);
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "s"), make_user("dr_b", "s")]);
|
||||
// Seed fixtures: dr_a has an open case, dr_b has one too.
|
||||
let case_a = config.data_path.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
let case_b = config.data_path.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
let case_a = config
|
||||
.data_path
|
||||
.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
let case_b = config
|
||||
.data_path
|
||||
.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
std::fs::create_dir_all(&case_a).unwrap();
|
||||
std::fs::create_dir_all(&case_b).unwrap();
|
||||
std::fs::write(case_a.join("2026-04-14T10-00-00Z.m4a"), b"x").unwrap();
|
||||
@@ -134,7 +162,11 @@ async fn cases_with_valid_cookie_shows_only_own_cases() {
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
// Log in as dr_a.
|
||||
let login = app.clone().oneshot(login_request("dr_a", "s")).await.unwrap();
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).unwrap();
|
||||
|
||||
let resp = app
|
||||
@@ -155,17 +187,20 @@ async fn cases_with_valid_cookie_shows_only_own_cases() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn case_detail_of_foreign_case_returns_404() {
|
||||
let config = test_config_with_users(vec![
|
||||
make_user("dr_a", "s"),
|
||||
make_user("dr_b", "s"),
|
||||
]);
|
||||
let case_b = config.data_path.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "s"), make_user("dr_b", "s")]);
|
||||
let case_b = config
|
||||
.data_path
|
||||
.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
std::fs::create_dir_all(&case_b).unwrap();
|
||||
std::fs::write(case_b.join("2026-04-14T11-00-00Z.m4a"), b"x").unwrap();
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let login = app.clone().oneshot(login_request("dr_a", "s")).await.unwrap();
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).unwrap();
|
||||
|
||||
let resp = app
|
||||
@@ -186,7 +221,11 @@ async fn logout_clears_session() {
|
||||
let config = test_config_with_users(vec![make_user("dr_a", "s")]);
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let login = app.clone().oneshot(login_request("dr_a", "s")).await.unwrap();
|
||||
let login = app
|
||||
.clone()
|
||||
.oneshot(login_request("dr_a", "s"))
|
||||
.await
|
||||
.unwrap();
|
||||
let cookie = extract_session_cookie(&login).unwrap();
|
||||
|
||||
let logout = app
|
||||
|
||||
@@ -82,9 +82,7 @@ fn get_with_if_none_match(uri: &str, etag: &str) -> Request<Body> {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn body_json(
|
||||
response: axum::http::Response<axum::body::Body>,
|
||||
) -> serde_json::Value {
|
||||
async fn body_json(response: axum::http::Response<axum::body::Body>) -> serde_json::Value {
|
||||
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -142,7 +140,13 @@ async fn empty_user_dir_returns_200_empty_list() {
|
||||
async fn recent_case_with_oneliner_shows_up() {
|
||||
let (config, dp) = test_config();
|
||||
let case_id = "550e8400-e29b-41d4-a716-000000000001";
|
||||
seed_case(&dp, case_id, Duration::from_secs(60), Some("Kniegelenk re., V.a. Meniskus")).await;
|
||||
seed_case(
|
||||
&dp,
|
||||
case_id,
|
||||
Duration::from_secs(60),
|
||||
Some("Kniegelenk re., V.a. Meniskus"),
|
||||
)
|
||||
.await;
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let response = app.oneshot(get("/api/oneliners")).await.unwrap();
|
||||
@@ -161,7 +165,13 @@ async fn recent_case_with_oneliner_shows_up() {
|
||||
#[tokio::test]
|
||||
async fn case_without_oneliner_shows_null() {
|
||||
let (config, dp) = test_config();
|
||||
seed_case(&dp, "550e8400-e29b-41d4-a716-000000000002", Duration::from_secs(60), None).await;
|
||||
seed_case(
|
||||
&dp,
|
||||
"550e8400-e29b-41d4-a716-000000000002",
|
||||
Duration::from_secs(60),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let body = body_json(app.oneshot(get("/api/oneliners")).await.unwrap()).await;
|
||||
@@ -202,10 +212,7 @@ async fn case_outside_default_but_inside_custom_window_included() {
|
||||
.await;
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let body = body_json(
|
||||
app.oneshot(get("/api/oneliners?hours=48")).await.unwrap(),
|
||||
)
|
||||
.await;
|
||||
let body = body_json(app.oneshot(get("/api/oneliners?hours=48")).await.unwrap()).await;
|
||||
assert_eq!(body["window_hours"], 48);
|
||||
assert_eq!(body["oneliners"].as_array().unwrap().len(), 1);
|
||||
|
||||
@@ -218,7 +225,9 @@ async fn hours_clamped_to_max() {
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let body = body_json(
|
||||
app.oneshot(get("/api/oneliners?hours=999999")).await.unwrap(),
|
||||
app.oneshot(get("/api/oneliners?hours=999999"))
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(body["window_hours"], 168);
|
||||
@@ -284,10 +293,7 @@ async fn etag_differs_between_different_hours() {
|
||||
.oneshot(get("/api/oneliners?hours=1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let r16 = app
|
||||
.oneshot(get("/api/oneliners?hours=16"))
|
||||
.await
|
||||
.unwrap();
|
||||
let r16 = app.oneshot(get("/api/oneliners?hours=16")).await.unwrap();
|
||||
|
||||
let etag1 = header_str(&r1, "etag");
|
||||
let etag16 = header_str(&r16, "etag");
|
||||
@@ -407,10 +413,7 @@ async fn sorts_by_last_recording_not_created() {
|
||||
.await;
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
let body = body_json(
|
||||
app.oneshot(get("/api/oneliners?hours=48")).await.unwrap(),
|
||||
)
|
||||
.await;
|
||||
let body = body_json(app.oneshot(get("/api/oneliners?hours=48")).await.unwrap()).await;
|
||||
let arr = body["oneliners"].as_array().unwrap();
|
||||
assert_eq!(arr.len(), 2);
|
||||
assert_eq!(arr[0]["case_id"], case_a, "A must sort first");
|
||||
|
||||
@@ -6,9 +6,9 @@ use std::sync::Arc;
|
||||
use doctate_server::config::{Config, User, WhisperUserSettings};
|
||||
use doctate_server::transcribe;
|
||||
use doctate_server::transcribe::ffmpeg::remux_faststart;
|
||||
use doctate_server::transcribe::ollama::{generate_oneliner, OllamaError};
|
||||
use doctate_server::transcribe::ollama::{OllamaError, generate_oneliner};
|
||||
use doctate_server::transcribe::recovery::scan_and_enqueue;
|
||||
use doctate_server::transcribe::whisper::{transcribe, WhisperError};
|
||||
use doctate_server::transcribe::whisper::{WhisperError, transcribe};
|
||||
use serde_json::json;
|
||||
use wiremock::matchers::{body_partial_json, method, path, query_param};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
@@ -24,9 +24,7 @@ async fn ffmpeg_remux_produces_valid_output() {
|
||||
let input = fixture("sample.m4a");
|
||||
assert!(input.exists(), "fixture missing: {}", input.display());
|
||||
|
||||
let output = remux_faststart(&input)
|
||||
.await
|
||||
.expect("remux failed");
|
||||
let output = remux_faststart(&input).await.expect("remux failed");
|
||||
|
||||
let meta = std::fs::metadata(output.path()).expect("output missing");
|
||||
assert!(meta.len() > 0, "output file is empty");
|
||||
@@ -45,7 +43,10 @@ async fn ffmpeg_remux_produces_valid_output() {
|
||||
.expect("ffprobe spawn failed");
|
||||
assert!(probe.status.success(), "ffprobe failed");
|
||||
let fmt = String::from_utf8_lossy(&probe.stdout);
|
||||
assert!(fmt.contains("mp4") || fmt.contains("m4a"), "unexpected format: {fmt}");
|
||||
assert!(
|
||||
fmt.contains("mp4") || fmt.contains("m4a"),
|
||||
"unexpected format: {fmt}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -134,7 +135,10 @@ async fn whisper_client_times_out() {
|
||||
.await
|
||||
.expect_err("expected timeout");
|
||||
|
||||
assert!(matches!(err, WhisperError::Http(_)), "expected Http error, got {err:?}");
|
||||
assert!(
|
||||
matches!(err, WhisperError::Http(_)),
|
||||
"expected Http error, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -170,8 +174,14 @@ async fn whisper_client_forwards_hotwords_and_prompt_when_set() {
|
||||
let received = server.received_requests().await.unwrap();
|
||||
let body = String::from_utf8_lossy(&received[0].body);
|
||||
assert!(body.contains("name=\"hotwords\""), "hotwords part missing");
|
||||
assert!(body.contains("HOCM Valsalva"), "hotwords value missing: {body}");
|
||||
assert!(body.contains("name=\"initial_prompt\""), "initial_prompt part missing");
|
||||
assert!(
|
||||
body.contains("HOCM Valsalva"),
|
||||
"hotwords value missing: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("name=\"initial_prompt\""),
|
||||
"initial_prompt part missing"
|
||||
);
|
||||
assert!(body.contains("Kardiologie"), "initial_prompt value missing");
|
||||
}
|
||||
|
||||
@@ -203,8 +213,14 @@ async fn whisper_client_omits_empty_optional_fields() {
|
||||
// Inspect the captured request to confirm absence of the optional parts.
|
||||
let received = server.received_requests().await.unwrap();
|
||||
let body = String::from_utf8_lossy(&received[0].body);
|
||||
assert!(!body.contains("name=\"hotwords\""), "hotwords part leaked: {body}");
|
||||
assert!(!body.contains("name=\"initial_prompt\""), "initial_prompt part leaked: {body}");
|
||||
assert!(
|
||||
!body.contains("name=\"hotwords\""),
|
||||
"hotwords part leaked: {body}"
|
||||
);
|
||||
assert!(
|
||||
!body.contains("name=\"initial_prompt\""),
|
||||
"initial_prompt part leaked: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -363,15 +379,24 @@ async fn ollama_oneliner_parses_chat_response() {
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/chat"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(ok_chat_body("HOCM mit Septum-Hypertrophie")))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(ok_chat_body("HOCM mit Septum-Hypertrophie")),
|
||||
)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let out = generate_oneliner(&client, &server.uri(), MODEL, 0, "irrelevant", Duration::from_secs(5))
|
||||
.await
|
||||
.expect("call failed");
|
||||
let out = generate_oneliner(
|
||||
&client,
|
||||
&server.uri(),
|
||||
MODEL,
|
||||
0,
|
||||
"irrelevant",
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await
|
||||
.expect("call failed");
|
||||
|
||||
assert_eq!(out, "HOCM mit Septum-Hypertrophie");
|
||||
}
|
||||
@@ -381,14 +406,23 @@ async fn ollama_oneliner_normalizes_response() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/chat"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(ok_chat_body("\"Kniegelenk re., V.a. Meniskus.\"\nSonstiges")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(ok_chat_body(
|
||||
"\"Kniegelenk re., V.a. Meniskus.\"\nSonstiges",
|
||||
)))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let out = generate_oneliner(&client, &server.uri(), MODEL, 0, "x", Duration::from_secs(5))
|
||||
.await
|
||||
.unwrap();
|
||||
let out = generate_oneliner(
|
||||
&client,
|
||||
&server.uri(),
|
||||
MODEL,
|
||||
0,
|
||||
"x",
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out, "Kniegelenk re., V.a. Meniskus");
|
||||
}
|
||||
|
||||
@@ -402,9 +436,16 @@ async fn ollama_oneliner_returns_status_on_500() {
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = generate_oneliner(&client, &server.uri(), MODEL, 0, "x", Duration::from_secs(5))
|
||||
.await
|
||||
.expect_err("expected error");
|
||||
let err = generate_oneliner(
|
||||
&client,
|
||||
&server.uri(),
|
||||
MODEL,
|
||||
0,
|
||||
"x",
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await
|
||||
.expect_err("expected error");
|
||||
match err {
|
||||
OllamaError::Status { status, body } => {
|
||||
assert_eq!(status, 500);
|
||||
@@ -430,9 +471,16 @@ async fn ollama_oneliner_sends_keep_alive_and_model() {
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let _ = generate_oneliner(&client, &server.uri(), MODEL, 0, "irrelevant", Duration::from_secs(5))
|
||||
.await
|
||||
.expect("call failed");
|
||||
let _ = generate_oneliner(
|
||||
&client,
|
||||
&server.uri(),
|
||||
MODEL,
|
||||
0,
|
||||
"irrelevant",
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await
|
||||
.expect("call failed");
|
||||
// Mock::expect(1) + server drop verifies the matcher hit exactly once.
|
||||
}
|
||||
|
||||
@@ -446,8 +494,15 @@ async fn ollama_oneliner_parse_error_on_missing_content() {
|
||||
.await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let err = generate_oneliner(&client, &server.uri(), MODEL, 0, "x", Duration::from_secs(5))
|
||||
.await
|
||||
.expect_err("expected parse error");
|
||||
let err = generate_oneliner(
|
||||
&client,
|
||||
&server.uri(),
|
||||
MODEL,
|
||||
0,
|
||||
"x",
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await
|
||||
.expect_err("expected parse error");
|
||||
assert!(matches!(err, OllamaError::Parse(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::http::{Request, StatusCode};
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
use doctate_server::config::{Config, User};
|
||||
use doctate_server::paths::{write_delete_marker, DeleteMarker, DELETE_MARKER};
|
||||
use doctate_server::paths::{DELETE_MARKER, DeleteMarker, write_delete_marker};
|
||||
|
||||
fn test_config() -> Arc<Config> {
|
||||
let data_path = std::env::temp_dir().join(format!(
|
||||
@@ -29,11 +29,7 @@ fn test_config() -> Arc<Config> {
|
||||
}
|
||||
|
||||
/// Build a multipart body with the given fields.
|
||||
fn multipart_body(
|
||||
case_id: &str,
|
||||
recorded_at: &str,
|
||||
audio: &[u8],
|
||||
) -> (String, Vec<u8>) {
|
||||
fn multipart_body(case_id: &str, recorded_at: &str, audio: &[u8]) -> (String, Vec<u8>) {
|
||||
let boundary = "----testboundary";
|
||||
let mut body = Vec::new();
|
||||
|
||||
@@ -417,7 +413,8 @@ async fn upload_resurrects_hard_deleted_case() {
|
||||
assert!(!case_dir.exists());
|
||||
|
||||
// Second upload with the same client-generated UUID.
|
||||
let (boundary, body) = multipart_body(case_id, "2026-04-13T12:00:00Z", b"resurrected recording");
|
||||
let (boundary, body) =
|
||||
multipart_body(case_id, "2026-04-13T12:00:00Z", b"resurrected recording");
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -442,4 +439,3 @@ async fn upload_resurrects_hard_deleted_case() {
|
||||
|
||||
let _ = std::fs::remove_dir_all(&data_path);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,7 @@ async fn web_index_empty() {
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/web/")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(Request::builder().uri("/web/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -65,19 +60,11 @@ async fn web_index_shows_cases() {
|
||||
|
||||
let open_case = data_path.join("dr_test").join(case1);
|
||||
std::fs::create_dir_all(&open_case).unwrap();
|
||||
std::fs::write(
|
||||
open_case.join("2026-04-13T10-30-00Z.m4a"),
|
||||
b"fake audio 1",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(open_case.join("2026-04-13T10-30-00Z.m4a"), b"fake audio 1").unwrap();
|
||||
|
||||
let done_case = data_path.join("dr_test").join(case2);
|
||||
std::fs::create_dir_all(&done_case).unwrap();
|
||||
std::fs::write(
|
||||
done_case.join("2026-04-12T09-00-00Z.m4a"),
|
||||
b"fake audio 2",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(done_case.join("2026-04-12T09-00-00Z.m4a"), b"fake audio 2").unwrap();
|
||||
std::fs::write(
|
||||
done_case.join("2026-04-12T09-00-00Z.transcript.txt"),
|
||||
"Herzkatheter ohne Befund.",
|
||||
@@ -87,12 +74,7 @@ async fn web_index_shows_cases() {
|
||||
|
||||
let app = doctate_server::create_router(config);
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/web/")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.oneshot(Request::builder().uri("/web/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user