//! `gatewayd` end to end: each turn's speaker is the Mattermost username of the one person who //! wrote every message of its burst, and there is none when two people did or the name cannot be //! found (M5c spec, sections 4 and 11). Do not edit. #[path = "support/fake_loop.rs"] mod fake_loop; #[path = "support/fake_mm.rs"] mod fake_mm; #[path = "support/gateway.rs"] mod gateway; #[path = "support/tmp.rs"] mod tmp; use std::sync::{Mutex, mpsc}; use std::time::{Duration, Instant}; use fake_loop::{done, serve_loop}; use fake_mm::{DM, EVE, FakeMm, KYLE, id, post}; use gateway::{SHARED, WAIT, loop_dir, read_state, start, up}; use gatewayd::config::Config; use tmp::TempDir; /// Kyle and Eve both allowed, in the shared channel. fn two_people(home: &TempDir, url: &str) -> Config { let text = format!( r#" [mattermost] url = "{url}" [secrets.mattermost_token] env = "NOT_READ_BY_RUN" [allow] users = ["{KYLE}", "{EVE}"] channels = ["{SHARED}"] [paths] home = "{}" [limits] typing_every_ms = 100 "#, home.path().display() ); Config::parse(&text).unwrap() } /// Wait until `gatewayd` has handled `post`: its id is in the state file's `recent`. The post is /// queued by then, since the event loop finishes a post before it looks at ended turns. fn wait_handled(home: &TempDir, post: &str) { let until = Instant::now() + WAIT; loop { let state = read_state(home); if state["recent"] .as_array() .is_some_and(|r| r.iter().any(|p| p == post)) { return; } assert!(Instant::now() < until, "{post} not handled: {state}"); std::thread::sleep(Duration::from_millis(10)); } } fn lookups(mm: &FakeMm, user: &str) -> usize { let path = format!("/api/v4/users/{user}"); mm.calls() .iter() .filter(|(m, p)| m == "GET" && *p == path) .count() } #[test] fn one_persons_message_has_their_username() { let home = TempDir::new("speaker-one"); let (mm, _running, turns, mut ws) = up(&home, Duration::ZERO); ws.posted(&post(&id('p', 1), KYLE, DM, "", "hello there", 5), "D"); let turn = turns.recv_timeout(WAIT).unwrap(); assert_eq!( (turn.content.as_str(), turn.speaker.as_deref()), ("hello there", Some("kyle")) ); assert_eq!(mm.wait_posts(1, WAIT).len(), 1); } #[test] fn a_username_is_looked_up_once() { let home = TempDir::new("speaker-cache"); let (mm, _running, turns, mut ws) = up(&home, Duration::ZERO); ws.posted(&post(&id('p', 1), KYLE, DM, "", "first", 5), "D"); ws.posted(&post(&id('p', 2), KYLE, DM, "", "second", 6), "D"); let speakers = [ turns.recv_timeout(WAIT).unwrap().speaker, turns.recv_timeout(WAIT).unwrap().speaker, ]; assert_eq!( speakers, [Some("kyle".to_string()), Some("kyle".to_string())] ); assert_eq!(lookups(&mm, KYLE), 1, "{:?}", mm.calls()); } #[test] fn bursts_have_a_speaker_only_when_one_person_wrote_them() { let home = TempDir::new("speaker-burst"); loop_dir(&home); let (release_tx, release_rx) = mpsc::channel::<()>(); let release = Mutex::new(release_rx); let turns = serve_loop(&home.path().join("run/loop/loop.sock"), move |n, turn| { if n <= 1 { let _ = release.lock().unwrap().recv_timeout(WAIT); } vec![done(&format!("answer to {}", turn.content))] }); let mm = FakeMm::start(); let _running = start(two_people(&home, &mm.url())); let mut ws = mm.next_ws(WAIT); let p1 = id('p', 1); ws.posted( &post(&p1, KYLE, SHARED, "", "@boxmaker-straylight start", 5), "O", ); let first = turns.recv_timeout(WAIT).unwrap(); assert_eq!( (first.content.as_str(), first.speaker.as_deref()), ("start", Some("kyle")) ); // Eve and Kyle while the first turn runs: one turn, from two people. ws.posted(&post(&id('p', 2), EVE, SHARED, &p1, "two", 6), "O"); ws.posted(&post(&id('p', 3), KYLE, SHARED, &p1, "three", 7), "O"); wait_handled(&home, &id('p', 3)); release_tx.send(()).unwrap(); let second = turns.recv_timeout(WAIT).unwrap(); assert_eq!( (second.content.as_str(), second.resume), ("two\n\nthree", true), "the text is joined as before" ); assert_eq!(second.speaker, None, "two people: no speaker"); // Eve alone while the second runs: her name, and nothing left over from the burst before. ws.posted(&post(&id('p', 4), EVE, SHARED, &p1, "four", 8), "O"); wait_handled(&home, &id('p', 4)); release_tx.send(()).unwrap(); let third = turns.recv_timeout(WAIT).unwrap(); assert_eq!( (third.content.as_str(), third.speaker.as_deref()), ("four", Some("user-e000")) ); assert_eq!(mm.wait_posts(3, WAIT).len(), 3); } #[test] fn a_name_that_cannot_be_found_leaves_the_turn_without_a_speaker() { let home = TempDir::new("speaker-missing"); let (mm, running, turns, mut ws) = up(&home, Duration::ZERO); mm.refuse_users(true); ws.posted(&post(&id('p', 1), KYLE, DM, "", "hello", 5), "D"); let turn = turns.recv_timeout(WAIT).unwrap(); assert_eq!( (turn.content.as_str(), turn.speaker), ("hello", None), "never the user id, never a guess" ); assert_eq!( mm.wait_posts(1, WAIT).len(), 1, "the turn is answered all the same" ); let line = format!( "gatewayd: no username for {KYLE}: status 404: \"{{\\\"message\\\":\\\"not found\\\"}}\"; \ the turn has no speaker" ); let log = running.wait_log("no username for"); assert_eq!( log.iter() .filter(|l| l.contains("no username")) .collect::>(), [&line] ); // A failure is not remembered: the next turn looks again. mm.refuse_users(false); ws.posted(&post(&id('p', 2), KYLE, DM, "", "again", 6), "D"); let again = turns.recv_timeout(WAIT).unwrap(); assert_eq!( (again.content.as_str(), again.speaker.as_deref()), ("again", Some("kyle")) ); assert_eq!(lookups(&mm, KYLE), 2, "{:?}", mm.calls()); }