//! End-to-end checks that boundary validators in `crate::validate` are //! actually wired into their respective handlers and produce the right //! response shape per endpoint. //! //! Per-validator unit tests live in `server/src/validate.rs`; this file //! tests the *integration* — the wiring, not the predicate. mod common; use axum::body::Body; use axum::http::{Request, StatusCode, header}; use tower::util::ServiceExt; use doctate_common::UPLOAD_PATH; use common::{ TestConfig, body_string, extract_session_cookie, login_request, multipart_upload_body, test_user, test_user_with_password, }; const API_KEY: &str = "key-dr_test"; fn upload_request(content_type: &str, body: Vec) -> Request { Request::builder() .method("POST") .uri(UPLOAD_PATH) .header("Content-Type", content_type) .header("X-API-Key", API_KEY) .body(Body::from(body)) .unwrap() } /// Drift like the Wear OS bug (microseconds) must be rejected with 400 /// at the upload boundary, not silently accepted into the filename. #[tokio::test] async fn upload_rejects_microsecond_recorded_at_with_400() { let cfg = TestConfig::new() .with_label("validate") .with_user(test_user("dr_test")) .build(); let app = doctate_server::create_router(cfg); let case_id = "550e8400-e29b-41d4-a716-446655440000"; let (ct, body) = multipart_upload_body(case_id, "2026-04-27T10:34:12.987072Z", b"audio"); let resp = app.oneshot(upload_request(&ct, body)).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } /// RFC3339 with offset (`+00:00`) is technically valid time, but our /// filename pipeline is `Z`-only — we reject anything else, even if a /// future client thinks it's being clever. #[tokio::test] async fn upload_rejects_offset_form_recorded_at_with_400() { let cfg = TestConfig::new() .with_label("validate") .with_user(test_user("dr_test")) .build(); let app = doctate_server::create_router(cfg); let case_id = "550e8400-e29b-41d4-a716-446655440000"; let (ct, body) = multipart_upload_body(case_id, "2026-04-27T10:34:12+00:00", b"audio"); let resp = app.oneshot(upload_request(&ct, body)).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } /// Sanity guard: the strict shape we *do* accept still works, so the /// validator hasn't accidentally locked everything out. #[tokio::test] async fn upload_accepts_strict_recorded_at_shape() { let cfg = TestConfig::new() .with_label("validate") .with_user(test_user("dr_test")) .build(); let app = doctate_server::create_router(cfg); let case_id = "550e8400-e29b-41d4-a716-446655440000"; let (ct, body) = multipart_upload_body(case_id, "2026-04-15T10:32:00Z", b"audio"); let resp = app.oneshot(upload_request(&ct, body)).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } /// A path-traversal-shaped slug must fold into the same "Login /// fehlgeschlagen" response as a wrong password — *not* leak via 400 /// that the slug shape was the problem. #[tokio::test] async fn login_with_traversal_slug_returns_login_failed_page() { let cfg = TestConfig::new() .with_user(test_user_with_password("dr_a", "secret")) .build(); let app = doctate_server::create_router(cfg); let resp = app .oneshot(login_request("../etc/passwd", "anything")) .await .unwrap(); // Same shape as wrong-password: 200 OK with the error page, no cookie. assert_eq!(resp.status(), StatusCode::OK); assert!(extract_session_cookie(&resp).is_none()); let body = body_string(resp).await; assert!(body.contains("Login fehlgeschlagen"), "got: {body}"); } /// Uppercase / non-ASCII slugs likewise must not bypass shape rules, /// even if they happen to coincide with no configured user. #[tokio::test] async fn login_with_uppercase_slug_returns_login_failed_page() { let cfg = TestConfig::new() .with_user(test_user_with_password("dr_a", "secret")) .build(); let app = doctate_server::create_router(cfg); let resp = app.oneshot(login_request("Dr_A", "secret")).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert!(extract_session_cookie(&resp).is_none()); } /// A malformed magic-link token must not 400 (would leak shape) — it /// should redirect to /login, identical to the unknown-token path. #[tokio::test] async fn magic_consume_with_malformed_token_redirects_to_login() { let cfg = TestConfig::new() .with_label("validate") .with_user(test_user("dr_a")) .build(); let app = doctate_server::create_router(cfg); // Too short — would never have come from `generate_token`. let resp = app .oneshot( Request::builder() .uri("/magic?token=abc") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::SEE_OTHER); let loc = resp .headers() .get(header::LOCATION) .expect("redirect must set Location") .to_str() .unwrap(); assert_eq!(loc, "/login"); } /// And the same for tokens that pass the length check but contain /// bytes outside the URL-safe alphabet. #[tokio::test] async fn magic_consume_with_non_urlsafe_token_redirects_to_login() { let cfg = TestConfig::new() .with_label("validate") .with_user(test_user("dr_a")) .build(); let app = doctate_server::create_router(cfg); let bad = "%2E%2E%2Fetc%2Fpasswd%2E%2E%2Fetc"; // url-encoded ./../etc/passwd… let uri = format!("/magic?token={bad}"); let resp = app .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(resp.status(), StatusCode::SEE_OTHER); }