//! Integration tests for the `/web/events` SSE route. //! //! We test two things end-to-end: //! 1. Without a session cookie, the route redirects to the login page //! (the web-auth extractor's standard rejection). //! 2. With a valid session, the route returns a streaming response with //! `Content-Type: text/event-stream`. //! //! Actual event-filtering logic (per-user scoping, admin sees-all) is //! covered at the unit level in `src/events.rs`; wiring the broadcast //! channel through an HTTP test would require reading a never-ending //! body, which these tests deliberately avoid. use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use axum::body::Body; use axum::http::{Request, StatusCode, header}; use tower::util::ServiceExt; use doctate_server::config::{Config, User}; fn unique_data_path() -> PathBuf { std::env::temp_dir().join(format!( "doctate-sse-test-{}-{}", std::process::id(), uuid::Uuid::new_v4() )) } fn make_user(slug: &str) -> User { User { slug: slug.into(), api_key: format!("key-{slug}"), // Password "s" — matches the login helper below. web_password: bcrypt::hash("s", 4).unwrap(), role: "doctor".into(), whisper: Default::default(), } } fn build_config(data_path: PathBuf) -> Arc { let users = vec![make_user("alice")]; let api_keys: HashMap = users .iter() .map(|u| (u.api_key.clone(), u.slug.clone())) .collect(); Arc::new(Config { data_path, users, api_keys, ..Config::test_default() }) } /// Log in via POST /web/login and return the resulting `session=…` cookie /// header value. Mirrors the helper in the other integration tests. async fn login(app: axum::Router, slug: &str) -> String { let body = format!("slug={slug}&password=s"); let resp = app .oneshot( Request::builder() .method("POST") .uri("/web/login") .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .body(Body::from(body)) .unwrap(), ) .await .unwrap(); for v in resp.headers().get_all(header::SET_COOKIE).iter() { let s = v.to_str().unwrap(); if let Some(pair) = s.split(';').next() && pair.starts_with("session=") { return pair.to_string(); } } panic!("no session cookie after login"); } #[tokio::test] async fn events_without_session_redirects_to_login() { let config = build_config(unique_data_path()); let app = doctate_server::create_router(config); let resp = app .oneshot( Request::builder() .method("GET") .uri("/web/events") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::FOUND); let location = resp .headers() .get(header::LOCATION) .and_then(|v| v.to_str().ok()) .unwrap_or(""); assert_eq!(location, "/web/login"); } #[tokio::test] async fn events_with_session_returns_sse_content_type() { let config = build_config(unique_data_path()); let app = doctate_server::create_router(config); let cookie = login(app.clone(), "alice").await; let resp = app .oneshot( Request::builder() .method("GET") .uri("/web/events") .header(header::COOKIE, cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let ct = resp .headers() .get(header::CONTENT_TYPE) .and_then(|v| v.to_str().ok()) .unwrap_or(""); assert!( ct.starts_with("text/event-stream"), "expected text/event-stream, got {ct:?}" ); }