Files
boxmaker/crates/gatewayd/tests/sessions.rs
T

402 lines
12 KiB
Rust

//! Which posts become turns, in which session, and how messages wait for a running turn (M4a spec,
//! section 7, including its table of examples), with the author of each (M5c spec, section 11).
//! Do not edit.
use bxcore::SessionId;
use gatewayd::mm::Post;
use gatewayd::sessions::{
ApprovalCommand, BUSY, Batch, Ignored, Message, Pushed, Queues, Route, Router, Thread,
UNKNOWN_COMMAND, named,
};
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");
assert_eq!(top.user, KYLE);
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, "hi", "the leading mention is stripped (M4b)");
}
#[test]
fn commands() {
let r = router();
let thread = Thread {
channel: DM.to_string(),
root: ROOT.to_string(),
};
for (message, command) in [
("!approve 42", ApprovalCommand::Approve(42)),
(
"!deny 42 not now",
ApprovalCommand::Deny(42, Some("not now".to_string())),
),
("!deny", ApprovalCommand::Malformed),
(
"@boxmaker-straylight !approve 7",
ApprovalCommand::Approve(7),
),
] {
assert_eq!(
r.route(&post(DM, ROOT, message), "D", &unknown),
Route::Approval {
thread: thread.clone(),
command,
user: KYLE.to_string(),
post: POST.to_string(),
at: 5,
},
"{message}"
);
}
for message in ["!approved", "!help", "!"] {
assert_eq!(
r.route(&post(DM, ROOT, message), "D", &unknown),
Route::Reply {
thread: thread.clone(),
text: UNKNOWN_COMMAND.to_string(),
},
"{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_by(KYLE, root, resume, text)
}
fn message_by(user: &str, 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,
user: user.to_string(),
}
}
fn senders(batch: &Batch) -> Vec<&str> {
batch.senders.iter().map(String::as_str).collect()
}
#[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(),
senders: vec![KYLE.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<String> = 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_by(EVE, ROOT, true, "c")),
Pushed::Full(m.thread.clone())
);
let next = q.finish(&m.session).unwrap();
assert_eq!(next.text, "a\n\nb");
assert_eq!(
senders(&next),
[KYLE, KYLE],
"the dropped message's author is not kept"
);
assert_eq!(next.sender(), Some(KYLE));
assert!(!BUSY.is_empty());
assert_eq!(q.finish(&SessionId::new("mm-never").unwrap()), None);
}
#[test]
fn a_queued_message_names_its_author() {
let r = Router::new(
BOT,
"boxmaker-straylight",
&[KYLE.to_string(), EVE.to_string()],
&[SHARED.to_string()],
);
for (text, expected) in [
("hello", "hello"),
("!!approve is a word", "!approve is a word"),
] {
let mut from_eve = post(DM, ROOT, text);
from_eve.user_id = EVE.to_string();
let m = queued(r.route(&from_eve, "D", &unknown));
assert_eq!(
(m.user.as_str(), m.text.as_str()),
(EVE, expected),
"{text}"
);
}
}
#[test]
fn a_batch_carries_the_author_of_each_message() {
let mut q = Queues::new(5);
let Pushed::Start(first) = q.push(message(ROOT, false, "one")) else {
panic!("not started")
};
assert_eq!((senders(&first), first.sender()), (vec![KYLE], Some(KYLE)));
assert_eq!(q.push(message_by(EVE, ROOT, true, "two")), Pushed::Waiting);
assert_eq!(q.push(message(ROOT, true, "three")), Pushed::Waiting);
let session = SessionId::new(&format!("mm-{ROOT}")).unwrap();
let mixed = q.finish(&session).unwrap();
assert_eq!(mixed.text, "two\n\nthree", "the text is joined as before");
assert_eq!(senders(&mixed), [EVE, KYLE]);
assert_eq!(mixed.sender(), None, "two people: no one sender");
assert_eq!(q.push(message_by(EVE, ROOT, true, "four")), Pushed::Waiting);
assert_eq!(q.push(message_by(EVE, ROOT, true, "five")), Pushed::Waiting);
let one = q.finish(&session).unwrap();
assert_eq!(
(senders(&one), one.sender()),
(vec![EVE, EVE], Some(EVE)),
"each batch has only its own messages' authors"
);
assert_eq!(q.finish(&session), None);
}
#[test]
fn a_batch_with_no_authors_has_no_sender() {
let batch = Batch {
session: SessionId::new(&format!("mm-{ROOT}")).unwrap(),
thread: Thread {
channel: DM.to_string(),
root: ROOT.to_string(),
},
resume: false,
text: "orphan".to_string(),
senders: Vec::new(),
};
assert_eq!(batch.sender(), None);
let three = Batch {
senders: vec![KYLE.to_string(), KYLE.to_string(), EVE.to_string()],
..batch
};
assert_eq!(three.sender(), None, "the last one differs");
}