gatewayd: a turn's speaker, the username of the one person who wrote its burst
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
//! `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::<Vec<_>>(),
|
||||
[&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());
|
||||
}
|
||||
Reference in New Issue
Block a user