1use std::convert::Infallible;
4use std::str::FromStr;
5use std::time::Duration;
6
7use axum::extract::{Query, State};
8use axum::response::sse::{Event as SseEvent, KeepAlive, Sse};
9use futures_util::stream::{Stream, StreamExt};
10use serde::Deserialize;
11use serde::de::{self, Deserializer};
12use serde_json::json;
13use tokio::sync::broadcast;
14use tokio_stream::wrappers::BroadcastStream;
15use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
16use uuid::Uuid;
17
18use crate::authorization::AuthorizeExt;
19use crate::error::ApiError;
20use crate::state::AppState;
21use ironflow_auth::extractor::Authenticated;
22use ironflow_engine::notify::Event;
23use ironflow_store::entities::ApiKeyScope;
24
25pub use ironflow_store::entities::EventKind;
26
27fn deserialize_comma_event_kinds<'de, D>(
29 deserializer: D,
30) -> Result<Option<Vec<EventKind>>, D::Error>
31where
32 D: Deserializer<'de>,
33{
34 let opt: Option<String> = Option::deserialize(deserializer)?;
35 match opt {
36 None => Ok(None),
37 Some(raw) => {
38 let kinds: Result<Vec<EventKind>, _> = raw
39 .split(',')
40 .map(|s| s.trim())
41 .filter(|s| !s.is_empty())
42 .map(EventKind::from_str)
43 .collect();
44 kinds.map(Some).map_err(de::Error::custom)
45 }
46 }
47}
48
49#[derive(Debug, Deserialize)]
64pub struct EventsQuery {
65 pub run_id: Option<Uuid>,
67 #[serde(default, deserialize_with = "deserialize_comma_event_kinds")]
69 pub types: Option<Vec<EventKind>>,
70}
71
72fn visible_to_non_admin(event: &Event) -> bool {
77 match event {
78 Event::RunCreated(_)
79 | Event::RunStatusChanged(_)
80 | Event::RunFailed(_)
81 | Event::RunBudgetExceeded(_)
82 | Event::RetryForced(_)
83 | Event::StepCompleted(_)
84 | Event::StepFailed(_)
85 | Event::ApprovalRequested(_)
86 | Event::ApprovalGranted(_)
87 | Event::ApprovalRejected(_)
88 | Event::ApprovalEscalated(_)
89 | Event::LogLine(_)
90 | Event::SignalAwaited(_)
91 | Event::SignalReceived(_) => true,
92 Event::UserSignedIn(_)
93 | Event::UserSignedUp(_)
94 | Event::UserSignedOut(_)
95 | Event::ProviderAccountUpdated(_)
96 | Event::ProviderAccountUsageUpdated(_)
97 | Event::ScheduleOccurrencesMissed(_)
98 | Event::WorkflowPaused(_)
99 | Event::WorkflowResumed(_) => false,
100 }
101}
102
103pub async fn events(
137 auth: Authenticated,
138 State(state): State<AppState>,
139 Query(query): Query<EventsQuery>,
140) -> Result<Sse<impl Stream<Item = Result<SseEvent, Infallible>>>, ApiError> {
141 auth.require(ApiKeyScope::RunsRead, false)?;
142 let connection = state.sse_limiter.try_acquire(auth.user_id)?;
143 let receiver = state.event_sender.subscribe();
144 let sees_every_event = auth.require(ApiKeyScope::Admin, true).is_ok();
147 let stream = event_stream(receiver, query.run_id, query.types, sees_every_event);
148 let stream = stream.map(move |item| {
150 let _held = &connection;
151 item
152 });
153
154 let sse = Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(30)));
155 Ok(sse)
156}
157
158const LAGGED_EVENT: &str = "lagged";
160
161fn event_stream(
167 receiver: broadcast::Receiver<Event>,
168 run_id_filter: Option<Uuid>,
169 type_filter: Option<Vec<EventKind>>,
170 sees_every_event: bool,
171) -> impl Stream<Item = Result<SseEvent, Infallible>> {
172 BroadcastStream::new(receiver).filter_map(move |result: Result<Event, _>| {
173 let type_filter = type_filter.clone();
174 async move {
175 let event = match result {
176 Ok(event) => event,
177 Err(BroadcastStreamRecvError::Lagged(missed)) => {
178 let data = json!({ "missed": missed }).to_string();
179 let sse_event = SseEvent::default().event(LAGGED_EVENT).data(data);
180 return Some(Ok(sse_event));
181 }
182 };
183
184 if !sees_every_event && !visible_to_non_admin(&event) {
185 return None;
186 }
187
188 if let Some(ref rid) = run_id_filter
189 && event.run_id() != Some(*rid)
190 {
191 return None;
192 }
193
194 if let Some(ref kinds) = type_filter {
195 let event_type = event.event_type();
196 if !kinds.iter().any(|k| k.as_str() == event_type) {
197 return None;
198 }
199 }
200
201 let data = serde_json::to_string(&event).ok()?;
202 let sse_event = SseEvent::default().event(event.event_type()).data(data);
203
204 Some(Ok::<_, Infallible>(sse_event))
205 }
206 })
207}
208
209#[cfg(test)]
210mod tests {
211 use std::collections::HashMap;
212 use std::sync::Arc;
213 use std::time::Duration;
214
215 use axum::Router;
216 use axum::body::Body;
217 use axum::response::IntoResponse;
218 use axum::response::sse::Sse;
219 use axum::routing::get;
220 use chrono::Utc;
221 use http_body_util::BodyExt;
222 use ironflow_auth::extractor::{API_KEY_PREFIX, API_KEY_SUFFIX_LEN};
223 use ironflow_auth::password;
224 use ironflow_core::providers::claude::ClaudeCodeProvider;
225 use ironflow_engine::engine::Engine;
226 use ironflow_engine::notify::{
227 Event, ProviderAccountChange, ProviderAccountUpdatedEvent, RunStatusChangedEvent,
228 UserSignedInEvent, UserSignedOutEvent, UserSignedUpEvent,
229 };
230 use ironflow_store::entities::{ApiKeyScope, NewApiKey, NewUser};
231 use ironflow_store::memory::InMemoryStore;
232 use ironflow_store::models::RunStatus;
233 use rust_decimal::Decimal;
234 use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
235 use tokio::net::{TcpListener, TcpStream};
236 use tokio::sync::broadcast;
237 use tokio::time::{sleep, timeout};
238 use uuid::Uuid;
239
240 use super::{EventKind, event_stream, events, visible_to_non_admin};
241 use crate::routes::test_helpers::create_user_auth_header;
242 use crate::sse_limiter::SseLimits;
243 use crate::state::AppState;
244
245 fn test_state() -> AppState {
246 let store = Arc::new(InMemoryStore::new());
247 let provider = Arc::new(ClaudeCodeProvider::new());
248 let engine = Arc::new(Engine::new(store.clone(), provider));
249 let jwt_config = Arc::new(ironflow_auth::jwt::JwtConfig {
250 secret: "test-secret".to_string(),
251 access_token_ttl_secs: 900,
252 refresh_token_ttl_secs: 604800,
253 cookie_domain: None,
254 cookie_secure: false,
255 });
256 let (event_sender, _) = broadcast::channel::<Event>(16);
257 AppState::new(
258 store,
259 engine,
260 jwt_config,
261 "test-worker-token".to_string(),
262 event_sender,
263 )
264 }
265
266 fn sample_run_event(run_id: Uuid) -> Event {
267 Event::RunStatusChanged(RunStatusChangedEvent {
268 run_id,
269 workflow_name: "deploy".to_string(),
270 from: RunStatus::Running,
271 to: RunStatus::Completed,
272 error: None,
273 cost_usd: Decimal::ZERO,
274 duration_ms: 1000,
275 labels: HashMap::new(),
276 at: Utc::now(),
277 })
278 }
279
280 fn sample_user_event() -> Event {
281 Event::UserSignedIn(UserSignedInEvent {
282 user_id: Uuid::now_v7(),
283 username: "alice".to_string(),
284 at: Utc::now(),
285 })
286 }
287
288 fn sample_user_signed_up_event() -> Event {
289 Event::UserSignedUp(UserSignedUpEvent {
290 user_id: Uuid::now_v7(),
291 username: "bob".to_string(),
292 at: Utc::now(),
293 })
294 }
295
296 fn sample_user_signed_out_event() -> Event {
297 Event::UserSignedOut(UserSignedOutEvent {
298 user_id: Uuid::now_v7(),
299 at: Utc::now(),
300 })
301 }
302
303 fn sample_provider_account_event() -> Event {
304 Event::ProviderAccountUpdated(ProviderAccountUpdatedEvent {
305 account_id: Uuid::now_v7(),
306 name: "acct".into(),
307 change: ProviderAccountChange::Updated,
308 at: Utc::now(),
309 })
310 }
311
312 async fn api_key_header(
318 state: &AppState,
319 owner_is_admin: bool,
320 scopes: Vec<ApiKeyScope>,
321 ) -> String {
322 let suffix = Uuid::now_v7().simple().to_string();
323 let owner = state
324 .store
325 .create_user(NewUser {
326 email: format!("owner-{suffix}@example.com"),
327 username: format!("owner-{suffix}"),
328 password_hash: "argon2hash".to_string(),
329 is_admin: Some(owner_is_admin),
330 })
331 .await
332 .unwrap();
333 let raw_key = format!("irfl_{}rest-of-secret-key", &suffix[24..]);
334 let prefix = &raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
335 state
336 .store
337 .create_api_key(NewApiKey {
338 user_id: owner.id,
339 name: format!("key-{suffix}"),
340 key_hash: password::hash(&raw_key).unwrap(),
341 key_prefix: prefix.to_string(),
342 scopes,
343 expires_at: None,
344 rate_limit_override: None,
345 })
346 .await
347 .unwrap();
348 format!("Bearer {raw_key}")
349 }
350
351 async fn start_sse_server(state: AppState) -> (String, broadcast::Sender<Event>, String) {
353 let sender = state.event_sender.clone();
354 let auth = create_user_auth_header(&state, "testuser", false).await;
355 let app = Router::new()
356 .route("/events", get(events))
357 .with_state(state);
358
359 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
360 let addr = listener.local_addr().unwrap().to_string();
361 tokio::spawn(async move {
362 axum::serve(listener, app).await.unwrap();
363 });
364 (addr, sender, auth)
365 }
366
367 async fn connect_sse(addr: &str, query: &str, auth: &str) -> BufReader<TcpStream> {
369 let stream = TcpStream::connect(addr).await.unwrap();
370 let (reader, mut writer) = stream.into_split();
371
372 use tokio::io::AsyncWriteExt;
373 writer
374 .write_all(
375 format!(
376 "GET /events{query} HTTP/1.1\r\nHost: {addr}\r\nAccept: text/event-stream\r\nAuthorization: {auth}\r\n\r\n"
377 )
378 .as_bytes(),
379 )
380 .await
381 .unwrap();
382
383 BufReader::new(reader.reunite(writer).unwrap())
384 }
385
386 async fn read_until_contains(
389 reader: &mut BufReader<TcpStream>,
390 needle: &str,
391 dur: Duration,
392 ) -> String {
393 let mut accumulated = String::new();
394 let result = timeout(dur, async {
395 loop {
396 let mut line = String::new();
397 let n = reader.read_line(&mut line).await.unwrap();
398 if n == 0 {
399 break;
400 }
401 accumulated.push_str(&line);
402 if accumulated.contains(needle) {
403 break;
404 }
405 }
406 })
407 .await;
408 if result.is_err() {
409 panic!("timeout waiting for '{needle}' in SSE stream. Data so far:\n{accumulated}");
410 }
411 accumulated
412 }
413
414 async fn wait_for_subscriber(sender: &broadcast::Sender<Event>) {
420 let ready = timeout(Duration::from_secs(5), async {
421 while sender.receiver_count() == 0 {
422 sleep(Duration::from_millis(5)).await;
423 }
424 })
425 .await;
426 assert!(ready.is_ok(), "SSE handler did not subscribe in time");
427 }
428
429 #[tokio::test]
430 async fn sse_stream_receives_events() {
431 let state = test_state();
432 let (addr, sender, auth) = start_sse_server(state).await;
433 let mut reader = connect_sse(&addr, "", &auth).await;
434
435 wait_for_subscriber(&sender).await;
436
437 let run_id = Uuid::now_v7();
438 sender.send(sample_run_event(run_id)).unwrap();
439
440 let text =
441 read_until_contains(&mut reader, &run_id.to_string(), Duration::from_secs(5)).await;
442
443 assert!(text.contains("run_status_changed"));
444 assert!(text.contains(&run_id.to_string()));
445 }
446
447 #[tokio::test]
448 async fn sse_filters_by_run_id() {
449 let state = test_state();
450 let (addr, sender, auth) = start_sse_server(state).await;
451
452 let target_run = Uuid::now_v7();
453 let other_run = Uuid::now_v7();
454
455 let mut reader = connect_sse(&addr, &format!("?run_id={target_run}"), &auth).await;
456 wait_for_subscriber(&sender).await;
457
458 sender.send(sample_run_event(other_run)).unwrap();
459 sender.send(sample_run_event(target_run)).unwrap();
460
461 let text =
462 read_until_contains(&mut reader, &target_run.to_string(), Duration::from_secs(5)).await;
463
464 assert!(text.contains(&target_run.to_string()));
465 assert!(!text.contains(&other_run.to_string()));
466 }
467
468 #[tokio::test]
469 async fn sse_filters_by_event_type() {
470 let state = test_state();
471 let admin = create_user_auth_header(&state, "admin", true).await;
473 let (addr, sender, _member) = start_sse_server(state).await;
474
475 let mut reader = connect_sse(&addr, "?types=user_signed_in", &admin).await;
476 wait_for_subscriber(&sender).await;
477
478 let run_id = Uuid::now_v7();
479 sender.send(sample_run_event(run_id)).unwrap();
480 sender.send(sample_user_event()).unwrap();
481
482 let text = read_until_contains(&mut reader, "user_signed_in", Duration::from_secs(5)).await;
483
484 assert!(text.contains("user_signed_in"));
485 assert!(!text.contains("run_status_changed"));
486 }
487
488 #[tokio::test]
489 async fn sse_returns_correct_content_type() {
490 let state = test_state();
491 let (addr, _sender, auth) = start_sse_server(state).await;
492 let mut reader = connect_sse(&addr, "", &auth).await;
493
494 let text =
495 read_until_contains(&mut reader, "text/event-stream", Duration::from_secs(5)).await;
496
497 assert!(text.contains("text/event-stream"));
498 }
499
500 #[tokio::test]
501 async fn sse_rejects_unauthenticated() {
502 let state = test_state();
503 let (addr, _sender, _auth) = start_sse_server(state).await;
504 let stream = TcpStream::connect(&addr).await.unwrap();
506 let (reader, mut writer) = stream.into_split();
507
508 use tokio::io::AsyncWriteExt;
509 writer
510 .write_all(
511 format!(
512 "GET /events HTTP/1.1\r\nHost: {addr}\r\nAccept: text/event-stream\r\n\r\n"
513 )
514 .as_bytes(),
515 )
516 .await
517 .unwrap();
518
519 let mut buf_reader = BufReader::new(reader.reunite(writer).unwrap());
520 let text = read_until_contains(&mut buf_reader, "401", Duration::from_secs(5)).await;
521
522 assert!(text.contains("401"));
523 }
524
525 async fn assert_provider_account_events_hidden(state: AppState, auth: &str) {
531 let (addr, sender, _jwt) = start_sse_server(state).await;
532 let mut reader = connect_sse(&addr, "", auth).await;
533 wait_for_subscriber(&sender).await;
534
535 let run_id = Uuid::now_v7();
536 sender.send(sample_provider_account_event()).unwrap();
537 sender.send(sample_run_event(run_id)).unwrap();
538
539 let text =
540 read_until_contains(&mut reader, &run_id.to_string(), Duration::from_secs(5)).await;
541
542 assert!(!text.contains("provider_account"));
543 }
544
545 #[tokio::test]
546 async fn sse_scope_hides_provider_account_events_from_a_runs_read_key() {
547 let state = test_state();
548 let auth = api_key_header(&state, true, vec![ApiKeyScope::RunsRead]).await;
549 assert_provider_account_events_hidden(state, &auth).await;
550 }
551
552 #[tokio::test]
553 async fn sse_scope_hides_provider_account_events_from_an_admin_scope_key_of_a_member() {
554 let state = test_state();
555 let auth = api_key_header(&state, false, vec![ApiKeyScope::Admin]).await;
556 assert_provider_account_events_hidden(state, &auth).await;
557 }
558
559 #[tokio::test]
560 async fn sse_scope_streams_provider_account_events_to_an_admin_scope_key() {
561 let state = test_state();
562 let auth = api_key_header(&state, true, vec![ApiKeyScope::Admin]).await;
563 let (addr, sender, _jwt) = start_sse_server(state).await;
564 let mut reader = connect_sse(&addr, "", &auth).await;
565 wait_for_subscriber(&sender).await;
566
567 sender.send(sample_provider_account_event()).unwrap();
568
569 let text =
570 read_until_contains(&mut reader, "provider_account", Duration::from_secs(5)).await;
571
572 assert!(text.contains("provider_account"));
573 }
574
575 #[tokio::test]
576 async fn sse_scope_rejects_a_key_without_runs_read() {
577 let state = test_state();
578 let auth = api_key_header(&state, true, vec![ApiKeyScope::StatsRead]).await;
579 let (addr, _sender, _jwt) = start_sse_server(state).await;
580 let mut reader = connect_sse(&addr, "", &auth).await;
581
582 let text = read_until_contains(&mut reader, "\r\n", Duration::from_secs(5)).await;
583
584 assert!(text.starts_with("HTTP/1.1 403"), "status line: {text}");
585 }
586
587 async fn assert_user_events_hidden(state: AppState, auth: Option<&str>) {
590 let (addr, sender, member) = start_sse_server(state).await;
591 let mut reader = connect_sse(&addr, "", auth.unwrap_or(&member)).await;
592 wait_for_subscriber(&sender).await;
593
594 let run_id = Uuid::now_v7();
595 sender.send(sample_user_event()).unwrap();
596 sender.send(sample_user_signed_up_event()).unwrap();
597 sender.send(sample_user_signed_out_event()).unwrap();
598 sender.send(sample_run_event(run_id)).unwrap();
599
600 let text =
601 read_until_contains(&mut reader, &run_id.to_string(), Duration::from_secs(5)).await;
602
603 assert!(!text.contains("user_signed_in"), "stream: {text}");
604 assert!(!text.contains("user_signed_up"), "stream: {text}");
605 assert!(!text.contains("user_signed_out"), "stream: {text}");
606 assert!(text.contains("run_status_changed"));
607 }
608
609 #[tokio::test]
610 async fn sse_hides_user_sign_in_sign_up_and_sign_out_events_from_a_member() {
611 assert_user_events_hidden(test_state(), None).await;
612 }
613
614 #[tokio::test]
615 async fn sse_hides_user_events_from_a_runs_read_key_of_an_admin() {
616 let state = test_state();
617 let auth = api_key_header(&state, true, vec![ApiKeyScope::RunsRead]).await;
618 assert_user_events_hidden(state, Some(&auth)).await;
619 }
620
621 #[tokio::test]
622 async fn sse_streams_user_events_to_an_admin() {
623 let state = test_state();
624 let admin = create_user_auth_header(&state, "admin", true).await;
625 let (addr, sender, _member) = start_sse_server(state).await;
626 let mut reader = connect_sse(&addr, "", &admin).await;
627 wait_for_subscriber(&sender).await;
628
629 sender.send(sample_user_event()).unwrap();
630
631 let text = read_until_contains(&mut reader, "user_signed_in", Duration::from_secs(5)).await;
632
633 assert!(text.contains("user_signed_in"));
634 }
635
636 #[test]
637 fn visible_to_non_admin_allows_only_run_events() {
638 assert!(visible_to_non_admin(&sample_run_event(Uuid::now_v7())));
639 assert!(!visible_to_non_admin(&sample_user_event()));
640 assert!(!visible_to_non_admin(&sample_user_signed_up_event()));
641 assert!(!visible_to_non_admin(&sample_user_signed_out_event()));
642 assert!(!visible_to_non_admin(&sample_provider_account_event()));
643 }
644
645 fn lagging_receiver() -> (broadcast::Sender<Event>, broadcast::Receiver<Event>) {
648 let (sender, receiver) = broadcast::channel::<Event>(2);
649 for _ in 0..5 {
650 sender.send(sample_run_event(Uuid::now_v7())).unwrap();
651 }
652 (sender, receiver)
653 }
654
655 async fn read_body_until(body: &mut Body, needle: &str) -> String {
657 let mut accumulated = String::new();
658 let result = timeout(Duration::from_secs(5), async {
659 while !accumulated.contains(needle) {
660 let Some(frame) = body.frame().await else {
661 break;
662 };
663 if let Ok(data) = frame.unwrap().into_data() {
664 accumulated.push_str(&String::from_utf8_lossy(&data));
665 }
666 }
667 })
668 .await;
669 assert!(
670 result.is_ok(),
671 "timeout waiting for '{needle}'. Data so far:\n{accumulated}"
672 );
673 accumulated
674 }
675
676 #[tokio::test]
677 async fn lagged_event_is_sent_when_the_subscriber_falls_behind() {
678 let (sender, receiver) = lagging_receiver();
679 let response = Sse::new(event_stream(receiver, None, None, false)).into_response();
680 let mut body = response.into_body();
681
682 let text = read_body_until(&mut body, "\"missed\"").await;
683 assert!(text.contains("lagged"), "got {text}");
684 assert!(text.contains("\"missed\":3"), "got {text}");
685
686 let run_id = Uuid::now_v7();
688 sender.send(sample_run_event(run_id)).unwrap();
689 let text = read_body_until(&mut body, &run_id.to_string()).await;
690 assert!(text.contains("run_status_changed"), "got {text}");
691 }
692
693 #[tokio::test]
694 async fn lagged_event_bypasses_the_run_and_type_filters() {
695 let (_sender, receiver) = lagging_receiver();
696 let stream = event_stream(
697 receiver,
698 Some(Uuid::now_v7()),
699 Some(vec![EventKind::UserSignedIn]),
700 false,
701 );
702 let mut body = Sse::new(stream).into_response().into_body();
703
704 let text = read_body_until(&mut body, "\"missed\"").await;
705 assert!(text.contains("lagged"), "got {text}");
706 assert!(!text.contains("run_status_changed"), "got {text}");
707 }
708
709 async fn status_line(reader: &mut BufReader<TcpStream>) -> String {
711 read_until_contains(reader, "\r\n", Duration::from_secs(5)).await
712 }
713
714 async fn read_body(reader: &mut BufReader<TcpStream>) -> String {
716 timeout(Duration::from_secs(5), async {
717 let mut content_length = 0;
718 loop {
719 let mut line = String::new();
720 reader.read_line(&mut line).await.unwrap();
721 if line.is_empty() || line == "\r\n" {
722 break;
723 }
724 if let Some(value) = line.to_ascii_lowercase().strip_prefix("content-length:") {
725 content_length = value.trim().parse().unwrap();
726 }
727 }
728 let mut body = vec![0; content_length];
729 reader.read_exact(&mut body).await.unwrap();
730 String::from_utf8(body).unwrap()
731 })
732 .await
733 .expect("timeout reading the response body")
734 }
735
736 fn limited_state(per_principal: usize) -> AppState {
737 test_state().with_sse_limits(SseLimits {
738 per_principal: Some(per_principal),
739 global: None,
740 })
741 }
742
743 #[tokio::test]
744 async fn events_returns_429_past_the_per_principal_limit() {
745 let state = limited_state(2);
746 let other_auth = create_user_auth_header(&state, "otheruser", false).await;
747 let (addr, _sender, auth) = start_sse_server(state).await;
748
749 let mut first = connect_sse(&addr, "", &auth).await;
750 assert!(status_line(&mut first).await.starts_with("HTTP/1.1 200"));
751 let mut second = connect_sse(&addr, "", &auth).await;
752 assert!(status_line(&mut second).await.starts_with("HTTP/1.1 200"));
753
754 let mut third = connect_sse(&addr, "", &auth).await;
755 let status = status_line(&mut third).await;
756 assert!(status.starts_with("HTTP/1.1 429"), "status line: {status}");
757 let body = read_body(&mut third).await;
758 assert!(body.contains("SSE_CONNECTION_LIMIT"), "body: {body}");
759
760 let mut other = connect_sse(&addr, "", &other_auth).await;
761 let status = status_line(&mut other).await;
762 assert!(
763 status.starts_with("HTTP/1.1 200"),
764 "another user keeps its own quota: {status}"
765 );
766 }
767
768 #[tokio::test]
769 async fn events_frees_the_slot_when_the_client_disconnects() {
770 let state = limited_state(1);
771 let (addr, _sender, auth) = start_sse_server(state).await;
772
773 let mut first = connect_sse(&addr, "", &auth).await;
774 assert!(status_line(&mut first).await.starts_with("HTTP/1.1 200"));
775 let mut refused = connect_sse(&addr, "", &auth).await;
776 assert!(status_line(&mut refused).await.starts_with("HTTP/1.1 429"));
777
778 drop(first);
779
780 let reopened = timeout(Duration::from_secs(5), async {
781 loop {
782 let mut retry = connect_sse(&addr, "", &auth).await;
783 if status_line(&mut retry).await.starts_with("HTTP/1.1 200") {
784 return retry;
785 }
786 sleep(Duration::from_millis(20)).await;
787 }
788 })
789 .await;
790 assert!(reopened.is_ok(), "the slot was not freed after disconnect");
791 }
792}