661ea6215e
Introduce a `preview_lines` setting in `users.toml` to control the number of visible lines for the analysis preview in the case list. This feature extracts the first paragraph of the `document.md` file, strips markdown formatting, and displays it, capped by the configured `preview_lines`. The actual line clamping is handled client-side via CSS `line-clamp`.
140 lines
4.0 KiB
Rust
140 lines
4.0 KiB
Rust
//! 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(),
|
|
retention: Default::default(),
|
|
window_hours: 72,
|
|
preview_lines: 2,
|
|
}
|
|
}
|
|
|
|
fn build_config(data_path: PathBuf) -> Arc<Config> {
|
|
let users = vec![make_user("alice")];
|
|
let api_keys: HashMap<String, String> = 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:?}"
|
|
);
|
|
}
|