f957d9ddc6
Reverse proxies (OpenResty in front of minerva) buffer text/event-stream by default, holding events until the response ends. An SSE stream never ends, so the browser stays silent and WebUI pages never live-reload when reached via app.doctate.de — while a direct minerva.lan:3000 client (bypassing the proxy) works, explaining the desktop/Chromebook split. Emit X-Accel-Buffering: no on the /events response so the proxy streams it unbuffered; a direct client ignores the header. Return type widens from Sse<..> to impl IntoResponse to carry the header tuple. Regression guard: sse_integration asserts the header is present. refs #12
93 lines
2.8 KiB
Rust
93 lines
2.8 KiB
Rust
//! Integration tests for the `/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.
|
|
|
|
mod common;
|
|
|
|
use axum::body::Body;
|
|
use axum::http::{Request, StatusCode, header};
|
|
use tower::util::ServiceExt;
|
|
|
|
use common::{TestConfig, get_with_cookie, header_opt, paths, test_user};
|
|
|
|
fn test_app() -> axum::Router {
|
|
let cfg = TestConfig::new()
|
|
.with_label("sse")
|
|
.with_user(test_user("alice"))
|
|
.build();
|
|
doctate_server::create_router(cfg)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn events_without_session_redirects_to_login() {
|
|
let app = test_app();
|
|
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri(paths::EVENTS)
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
|
assert_eq!(
|
|
header_opt(&resp, header::LOCATION.as_str()),
|
|
Some(paths::LOGIN)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn events_with_session_returns_sse_content_type() {
|
|
let app = test_app();
|
|
let cookie = common::login(&app, "alice", "s").await;
|
|
|
|
let resp = app
|
|
.oneshot(get_with_cookie(paths::EVENTS, &cookie))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let ct = header_opt(&resp, header::CONTENT_TYPE.as_str()).unwrap_or("");
|
|
assert!(
|
|
ct.starts_with("text/event-stream"),
|
|
"expected text/event-stream, got {ct:?}"
|
|
);
|
|
}
|
|
|
|
// Regression guard for #12: reverse proxies (nginx/OpenResty in front of
|
|
// minerva) buffer `text/event-stream` by default, which holds events in
|
|
// the proxy until the response ends — but an SSE response never ends, so
|
|
// the browser stays silent and the page never live-reloads. The
|
|
// `X-Accel-Buffering: no` response header tells the proxy to stream this
|
|
// one response unbuffered. A direct client ignores the header harmlessly.
|
|
#[tokio::test]
|
|
async fn events_disables_proxy_buffering() {
|
|
let app = test_app();
|
|
let cookie = common::login(&app, "alice", "s").await;
|
|
|
|
let resp = app
|
|
.oneshot(get_with_cookie(paths::EVENTS, &cookie))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
assert_eq!(
|
|
header_opt(&resp, "x-accel-buffering"),
|
|
Some("no"),
|
|
"SSE response must disable reverse-proxy buffering"
|
|
);
|
|
}
|