//! Which posts become turns, in which session, and how messages wait for a running turn (M4a spec, //! section 7, including its table of examples). Do not edit. use gatewayd::mm::Post; use gatewayd::sessions::{ BUSY, Batch, Ignored, M4B_COMMAND, Message, Pushed, Queues, Route, Router, Thread, UNKNOWN_COMMAND, named, }; use proto::SessionId; const BOT: &str = "b0000000000000000000000000"; const KYLE: &str = "k0000000000000000000000000"; const EVE: &str = "e0000000000000000000000000"; const DM: &str = "d0000000000000000000000000"; const SHARED: &str = "c0000000000000000000000000"; const OTHER: &str = "o0000000000000000000000000"; const ROOT: &str = "r0000000000000000000000000"; const POST: &str = "p0000000000000000000000000"; fn router() -> Router { Router::new( BOT, "boxmaker-straylight", &[KYLE.to_string()], &[SHARED.to_string()], ) } fn post(channel: &str, root: &str, message: &str) -> Post { Post { id: POST.to_string(), user_id: KYLE.to_string(), channel_id: channel.to_string(), root_id: root.to_string(), message: message.to_string(), create_at: 5, delete_at: 0, kind: String::new(), } } fn known(root: &str) -> bool { root == ROOT } fn unknown(_: &str) -> bool { false } fn queued(route: Route) -> Message { match route { Route::Queue(m) => m, other => panic!("not queued: {other:?}"), } } #[test] fn the_examples_in_the_spec() { let r = router(); let cases = [ ("", "@boxmaker-straylight summarise the audit log", true), (ROOT, "and the older files?", true), (ROOT, "@hermes what do you think?", false), ("", "@boxmaker-straylight @hermes compare notes", true), ("", "@boxmaker-straylightx hello", false), ("", "@channel standup in five", false), ]; for (root, message, yes) in cases { let got = r.route(&post(SHARED, root, message), "O", &known); assert_eq!(matches!(got, Route::Queue(_)), yes, "{message}: {got:?}"); if !yes { assert_eq!(got, Route::Ignore(Ignored::NotForUs), "{message}"); } } } #[test] fn naming() { assert_eq!(named("@Boxmaker-Straylight, look"), ["boxmaker-straylight"]); assert_eq!(named("ask @boxmaker-straylight."), ["boxmaker-straylight"]); assert_eq!(named("@a.b_c-d... and @e"), ["a.b_c-d", "e"]); assert_eq!(named("@ alone, @@x, trailing @"), ["x"]); assert_eq!(named("ünïcødé @ʙob @bob"), ["bob"]); assert!(named("no names here").is_empty()); let r = router(); for message in [ "hi @BOXMAKER-STRAYLIGHT", "@boxmaker-straylight.", "(@boxmaker-straylight)", ] { assert!( matches!( r.route(&post(SHARED, "", message), "P", &unknown), Route::Queue(_) ), "{message}" ); } } #[test] fn replies_in_our_thread_that_name_everyone_are_still_ours() { let r = router(); for message in ["@here any news?", "@all done", "thanks @channel"] { assert!( matches!( r.route(&post(SHARED, ROOT, message), "O", &known), Route::Queue(_) ), "{message}" ); } let got = r.route( &post( SHARED, "q0000000000000000000000000", "a reply in someone else's thread", ), "O", &known, ); assert_eq!(got, Route::Ignore(Ignored::NotForUs)); } #[test] fn who_and_where() { let r = router(); let mut own = post(DM, "", "hello"); own.user_id = BOT.to_string(); assert_eq!(r.route(&own, "D", &unknown), Route::Ignore(Ignored::Own)); let mut system = post(DM, "", "joined"); system.kind = "system_join_channel".to_string(); assert_eq!( r.route(&system, "D", &unknown), Route::Ignore(Ignored::System) ); let mut stranger = post(DM, "", "@boxmaker-straylight hello"); stranger.user_id = EVE.to_string(); assert_eq!( r.route(&stranger, "D", &unknown), Route::Ignore(Ignored::NotAllowed) ); let mut stranger_cmd = post(DM, "", "!approve 1"); stranger_cmd.user_id = EVE.to_string(); assert_eq!( r.route(&stranger_cmd, "D", &unknown), Route::Ignore(Ignored::NotAllowed) ); let naming = "@boxmaker-straylight hello"; assert_eq!( r.route(&post(OTHER, "", naming), "O", &unknown), Route::Ignore(Ignored::NotForUs) ); assert_eq!( r.route(&post(SHARED, "", naming), "X", &unknown), Route::Ignore(Ignored::NotForUs) ); assert!(matches!( r.route(&post(SHARED, "", naming), "G", &unknown), Route::Queue(_) )); assert!(matches!( r.route(&post(DM, "", "no name needed"), "D", &unknown), Route::Queue(_) )); } #[test] fn sessions_and_threads() { let r = router(); let top = queued(r.route(&post(DM, "", "hello"), "D", &unknown)); assert_eq!(top.session.as_str(), format!("mm-{POST}")); assert_eq!( top.thread, Thread { channel: DM.to_string(), root: POST.to_string() } ); assert!(!top.resume && !top.joins_thread); assert_eq!(top.text, "hello"); let reply = queued(r.route(&post(DM, ROOT, "more"), "D", &unknown)); assert_eq!(reply.session.as_str(), format!("mm-{ROOT}")); assert_eq!(reply.thread.root, ROOT); assert!(reply.resume); let channel = queued(r.route(&post(SHARED, "", "@boxmaker-straylight hi"), "O", &unknown)); assert!(channel.joins_thread && !channel.resume); assert_eq!(channel.text, "@boxmaker-straylight hi"); } #[test] fn commands() { let r = router(); let thread = Thread { channel: DM.to_string(), root: ROOT.to_string(), }; let reply = |text: &str| Route::Reply { thread: thread.clone(), text: text.to_string(), }; for (message, answer) in [ ("!approve 42", M4B_COMMAND), ("!deny 42 not now", M4B_COMMAND), ("!deny", M4B_COMMAND), ("!approved", UNKNOWN_COMMAND), ("!help", UNKNOWN_COMMAND), ("!", UNKNOWN_COMMAND), ] { assert_eq!( r.route(&post(DM, ROOT, message), "D", &unknown), reply(answer), "{message}" ); } assert_eq!( queued(r.route(&post(DM, ROOT, "!!approve is a word"), "D", &unknown)).text, "!approve is a word" ); assert_eq!( queued(r.route(&post(DM, ROOT, "!!"), "D", &unknown)).text, "!" ); assert_eq!( queued(r.route(&post(DM, ROOT, " !help"), "D", &unknown)).text, " !help" ); } fn message(root: &str, resume: bool, text: &str) -> Message { Message { session: SessionId::new(&format!("mm-{root}")).unwrap(), thread: Thread { channel: DM.to_string(), root: root.to_string(), }, resume, text: text.to_string(), joins_thread: false, } } #[test] fn messages_wait_for_the_running_turn_and_go_together() { let mut q = Queues::new(3); let first = message(ROOT, false, "one"); let Pushed::Start(batch) = q.push(first.clone()) else { panic!("not started") }; assert_eq!( batch, Batch { session: first.session.clone(), thread: first.thread.clone(), resume: false, text: "one".to_string() } ); assert_eq!(q.push(message(ROOT, true, "two")), Pushed::Waiting); assert_eq!(q.push(message(ROOT, true, "three\nlines")), Pushed::Waiting); let other = message(POST, false, "elsewhere"); assert!( matches!(q.push(other.clone()), Pushed::Start(_)), "another session starts at once" ); assert_eq!(q.running(), 2); let mut roots: Vec = q.threads().into_iter().map(|t| t.root).collect(); roots.sort(); assert_eq!(roots, [POST, ROOT]); let next = q.finish(&first.session).unwrap(); assert_eq!( (next.text.as_str(), next.resume), ("two\n\nthree\nlines", true) ); assert_eq!(q.finish(&first.session), None); assert_eq!(q.finish(&other.session), None); assert_eq!(q.running(), 0); assert!( matches!(q.push(message(ROOT, true, "later")), Pushed::Start(_)), "idle again" ); } #[test] fn a_full_queue_drops_the_message() { let mut q = Queues::new(2); let m = message(ROOT, false, "run"); assert!(matches!(q.push(m.clone()), Pushed::Start(_))); assert_eq!(q.push(message(ROOT, true, "a")), Pushed::Waiting); assert_eq!(q.push(message(ROOT, true, "b")), Pushed::Waiting); assert_eq!( q.push(message(ROOT, true, "c")), Pushed::Full(m.thread.clone()) ); assert_eq!(q.finish(&m.session).unwrap().text, "a\n\nb"); assert!(!BUSY.is_empty()); assert_eq!(q.finish(&SessionId::new("mm-never").unwrap()), None); }