//! `gatewayd` end to end, against a fake Mattermost and a fake `loopd`: who is answered, where, //! and how (M4a spec, sections 7 and 8). 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; use fake_loop::{done, serve_loop}; use fake_mm::{BOT, BOT_NAME, DM, EVE, EVE_DM, FakeMm, KYLE, id, post}; use gateway::{OTHER, SHARED, WAIT, config, loop_dir, read_state, start, up}; use gatewayd::serve::Stop; use gatewayd::sessions::{BUSY, M4B_COMMAND}; use tmp::TempDir; #[test] fn a_direct_message_is_answered_in_its_thread_while_typing() { let home = TempDir::new("serve-dm"); let (mm, running, turns, mut ws) = up(&home, Duration::from_millis(600)); let p1 = id('p', 1); ws.posted(&post(&p1, KYLE, DM, "", "hello there", 5), "D"); let turn = turns.recv_timeout(WAIT).unwrap(); assert_eq!( (turn.session.as_str(), turn.content.as_str(), turn.resume), (format!("mm-{p1}").as_str(), "hello there", false) ); let typing = ws.typing_within(Duration::from_millis(500)); assert!(typing.len() >= 2, "{typing:?}"); assert!( typing.iter().all(|(c, p)| c == DM && *p == p1), "{typing:?}" ); let posts = mm.wait_posts(1, WAIT); assert_eq!( posts, [( DM.to_string(), p1.clone(), "answer to hello there".to_string() )] ); // Typing sent just before the answer may still be on its way; after that, it stops. ws.typing_within(Duration::from_millis(300)); assert!( ws.typing_within(Duration::from_millis(400)).is_empty(), "typing stops after the answer" ); let log = running.log(); assert!( log.iter() .any(|l| l == &format!("gatewayd: connected to {} as {BOT_NAME}", mm.url())), "{log:?}" ); assert!(matches!(running.finish(), Stop::Asked)); } #[test] fn anyone_else_gets_nothing_at_all() { let home = TempDir::new("serve-stranger"); let (mm, running, turns, mut ws) = up(&home, Duration::ZERO); let p1 = id('p', 1); ws.posted(&post(&p1, EVE, EVE_DM, "", "secret words", 5), "D"); let mut own = post(&id('p', 2), BOT, DM, "", "my own post", 6); own["user_id"] = serde_json::json!(BOT); ws.posted(&own, "D"); let log = running.wait_log("not allowed"); assert!(turns.recv_timeout(Duration::from_millis(300)).is_err()); assert!(ws.typing_within(Duration::from_millis(200)).is_empty()); assert!(mm.posts().is_empty()); assert!( log.contains(&format!( "gatewayd: ignored post {p1} from {EVE}: not allowed" )), "{log:?}" ); assert!(!log.iter().any(|l| l.contains("secret words")), "{log:?}"); } #[test] fn messages_during_a_turn_go_together_in_the_next() { let home = TempDir::new("serve-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 == 0 { let _ = release.lock().unwrap().recv_timeout(WAIT); } vec![done(&format!("answer to {}", turn.content))] }); let mm = FakeMm::start(); let _running = start(config(&home, &mm.url(), "")); let mut ws = mm.next_ws(WAIT); let p1 = id('p', 1); ws.posted(&post(&p1, KYLE, DM, "", "one", 5), "D"); assert_eq!(turns.recv_timeout(WAIT).unwrap().content, "one"); let saved = read_state(&home); assert_eq!( saved["in_flight"], serde_json::json!([{"session": format!("mm-{p1}"), "channel": DM, "root": p1}]) ); ws.posted(&post(&id('p', 2), KYLE, DM, &p1, "two", 6), "D"); ws.posted(&post(&id('p', 3), KYLE, DM, &p1, "three", 7), "D"); std::thread::sleep(Duration::from_millis(200)); release_tx.send(()).unwrap(); let second = turns.recv_timeout(WAIT).unwrap(); assert_eq!( ( second.session.as_str(), second.content.as_str(), second.resume ), (format!("mm-{p1}").as_str(), "two\n\nthree", true) ); let posts = mm.wait_posts(2, WAIT); let texts: Vec<&str> = posts.iter().map(|(_, _, t)| t.as_str()).collect(); assert_eq!(texts, ["answer to one", "answer to two\n\nthree"]); } #[test] fn a_full_queue_says_busy() { let home = TempDir::new("serve-busy"); 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 |_, _| { let _ = release.lock().unwrap().recv_timeout(WAIT); vec![done("ok")] }); let mm = FakeMm::start(); let _running = start(config(&home, &mm.url(), "queue = 1")); let mut ws = mm.next_ws(WAIT); let p1 = id('p', 1); for (n, text) in ["run", "waits", "too many"].iter().enumerate() { let n = u32::try_from(n).unwrap(); let root = if n == 0 { String::new() } else { p1.clone() }; ws.posted( &post(&id('p', n + 1), KYLE, DM, &root, text, i64::from(n) + 5), "D", ); } let posts = mm.wait_posts(1, WAIT); assert_eq!(posts, [(DM.to_string(), p1, BUSY.to_string())]); release_tx.send(()).unwrap(); release_tx.send(()).unwrap(); } #[test] fn channels_are_answered_only_when_named_or_in_our_thread() { let home = TempDir::new("serve-channel"); let (mm, _running, turns, mut ws) = up(&home, Duration::ZERO); let p1 = id('p', 1); ws.posted( &post(&id('p', 9), KYLE, SHARED, "", "hello everyone", 4), "O", ); ws.posted( &post( &id('p', 8), KYLE, OTHER, "", "@boxmaker-straylight elsewhere", 4, ), "O", ); ws.posted( &post(&p1, KYLE, SHARED, "", "@boxmaker-straylight start", 5), "O", ); assert_eq!( turns.recv_timeout(WAIT).unwrap().content, "@boxmaker-straylight start" ); mm.wait_posts(1, WAIT); ws.posted( &post(&id('p', 2), KYLE, SHARED, &p1, "@hermes your turn", 6), "O", ); ws.posted(&post(&id('p', 3), KYLE, SHARED, &p1, "and more", 7), "O"); let next = turns.recv_timeout(WAIT).unwrap(); assert_eq!((next.content.as_str(), next.resume), ("and more", true)); let posts = mm.wait_posts(2, WAIT); assert!( posts.iter().all(|(c, r, _)| c == SHARED && *r == p1), "{posts:?}" ); assert!(turns.recv_timeout(Duration::from_millis(200)).is_err()); let saved = read_state(&home); assert!(saved["channels"].get(OTHER).is_none(), "{saved}"); assert_eq!(saved["threads"], serde_json::json!([p1])); } #[test] fn commands_are_answered_without_a_turn() { let home = TempDir::new("serve-command"); let (mm, _running, turns, mut ws) = up(&home, Duration::ZERO); let p1 = id('p', 1); ws.posted(&post(&p1, KYLE, DM, "", "!approve 3", 5), "D"); assert_eq!( mm.wait_posts(1, WAIT), [(DM.to_string(), p1, M4B_COMMAND.to_string())] ); assert!(turns.recv_timeout(Duration::from_millis(200)).is_err()); }