Files
boxmaker/crates/memoryd/tests/honcho_client.rs
T

440 lines
13 KiB
Rust

//! `memoryd`'s Honcho clients (M5c spec, sections 3 and 6), against a fake Honcho that answers
//! from the responses recorded from rift: the workspace, the peers and the session made before
//! anything is written, each once per run, with the fleet's settings and the agent unobserved;
//! the errors by kind and status, never a body. The keys are in `honcho_keys.rs`. Do not edit.
#[path = "support/honcho.rs"]
mod honcho;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::time::Duration;
use honcho::{FakeHoncho, Mode, Request};
use memoryd::honcho::client::{
CHAT_READ, CONNECT, Clients, HonchoError, READ, Timeouts, classify, fleet_configuration,
};
use memoryd::honcho::config::HonchoConfig;
use memoryd::honcho::ledger::Workspace;
use memoryd::honcho::naming::HonchoSession;
use serde_json::{Value, json};
const FAST: Timeouts = Timeouts {
connect: Duration::from_secs(5),
read: Duration::from_millis(300),
};
fn config(url: &str) -> HonchoConfig {
HonchoConfig {
url: url.to_string(),
agent: "boxmaker0".to_string(),
owner: "kyle".to_string(),
trusted: "boxmaker0-trusted".to_string(),
untrusted: "boxmaker0-untrusted".to_string(),
fleet: "fleet".to_string(),
read: Vec::new(),
keys: BTreeMap::new(),
}
}
fn no_env(_: &str) -> Option<OsString> {
None
}
fn clients(fake: &FakeHoncho) -> Clients {
Clients::new(&config(&fake.url), &no_env, FAST).unwrap().0
}
fn session(name: &str, speakers: &[&str]) -> HonchoSession {
HonchoSession {
name: name.to_string(),
speakers: speakers.iter().map(|s| s.to_string()).collect(),
}
}
/// A request as (method, path, body).
fn seen(requests: &[Request]) -> Vec<(String, String, Value)> {
requests
.iter()
.map(|r| {
(
r.method.clone(),
r.path.clone(),
r.body.clone().unwrap_or(Value::Null),
)
})
.collect()
}
fn post(path: &str, body: Value) -> (String, String, Value) {
("POST".to_string(), path.to_string(), body)
}
#[test]
fn the_timeouts() {
assert_eq!(CONNECT, Duration::from_secs(5));
assert_eq!(READ, Duration::from_secs(30));
assert_eq!(CHAT_READ, Duration::from_secs(150));
assert_eq!(
Timeouts::WRITES,
Timeouts {
connect: CONNECT,
read: READ
}
);
}
#[test]
fn each_client_has_its_workspace_and_timeouts() {
let fake = FakeHoncho::start();
let c = clients(&fake);
for (workspace, name) in [
(Workspace::Trusted, "boxmaker0-trusted"),
(Workspace::Untrusted, "boxmaker0-untrusted"),
(Workspace::Fleet, "fleet"),
] {
let client = c.client(workspace);
assert_eq!(client.workspace_id(), name);
// `roncho`'s `Debug` shows the timeouts, and whether there is a key, never the key.
let shown = format!("{client:?}");
assert!(shown.contains("connect_timeout: 5s"), "{shown}");
assert!(shown.contains("read_timeout: 300ms"), "{shown}");
assert!(shown.contains("api_key_present: false"), "{shown}");
}
}
#[test]
fn the_fleets_configuration_is_the_recorded_one() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/honcho/01-workspace-create.json"
);
let recorded: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
assert_eq!(
Value::Object(fleet_configuration()),
recorded["request"]["body"]["configuration"]
);
}
#[test]
fn a_session_is_made_after_its_workspace_and_peers() {
let fake = FakeHoncho::start();
let mut c = clients(&fake);
c.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
.unwrap();
let w = "/v3/workspaces/boxmaker0-trusted";
assert_eq!(
seen(&fake.requests()),
vec![
post("/v3/workspaces", json!({ "id": "boxmaker0-trusted" })),
post(
&format!("{w}/peers"),
json!({ "id": "boxmaker0", "configuration": { "observe_me": false } })
),
post(&format!("{w}/peers"), json!({ "id": "kyle" })),
post(
&format!("{w}/sessions"),
json!({ "id": "chat-1", "peers": {
"boxmaker0": { "observe_me": false, "observe_others": false },
"kyle": { "observe_me": true, "observe_others": false },
} })
),
]
);
}
#[test]
fn what_was_made_is_not_asked_for_again_in_this_run() {
let fake = FakeHoncho::start();
let mut c = clients(&fake);
c.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
.unwrap();
fake.clear();
c.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
.unwrap();
assert!(fake.requests().is_empty(), "{:?}", fake.requests());
// A new speaker: only its peer and the next session.
c.ensure_session(Workspace::Trusted, &session("chat-1-2", &["kyle", "ana_b"]))
.unwrap();
let w = "/v3/workspaces/boxmaker0-trusted";
assert_eq!(
seen(&fake.requests()),
vec![
post(&format!("{w}/peers"), json!({ "id": "ana_b" })),
post(
&format!("{w}/sessions"),
json!({ "id": "chat-1-2", "peers": {
"boxmaker0": { "observe_me": false, "observe_others": false },
"kyle": { "observe_me": true, "observe_others": false },
"ana_b": { "observe_me": true, "observe_others": false },
} })
),
]
);
// Another workspace is made sure of on its own.
fake.clear();
c.ensure_session(Workspace::Untrusted, &session("chat-1", &["kyle"]))
.unwrap();
assert_eq!(fake.requests().len(), 4);
assert!(
fake.requests()
.iter()
.skip(1)
.all(|r| r.path.starts_with("/v3/workspaces/boxmaker0-untrusted/"))
);
}
#[test]
fn after_a_restart_everything_is_made_sure_of_again() {
let fake = FakeHoncho::start();
clients(&fake)
.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
.unwrap();
fake.clear();
// Honcho answers 200 now: it has them all. That is success too.
clients(&fake)
.ensure_session(Workspace::Trusted, &session("chat-1", &["kyle"]))
.unwrap();
assert_eq!(fake.requests().len(), 4);
}
#[test]
fn the_fleet_is_made_with_its_settings_and_the_agent_alone() {
let fake = FakeHoncho::start();
let mut c = clients(&fake);
c.ensure_session(Workspace::Fleet, &session("chat-1", &[]))
.unwrap();
assert_eq!(
seen(&fake.requests()),
vec![
post(
"/v3/workspaces",
json!({ "id": "fleet", "configuration": {
"reasoning": { "enabled": false },
"summary": { "enabled": false },
"dream": { "enabled": false },
} })
),
post(
"/v3/workspaces/fleet/peers",
json!({ "id": "boxmaker0", "configuration": { "observe_me": false } })
),
post(
"/v3/workspaces/fleet/sessions",
json!({ "id": "chat-1", "peers": {
"boxmaker0": { "observe_me": false, "observe_others": false },
} })
),
]
);
}
#[test]
fn a_409_on_create_means_it_is_there() {
let fake = FakeHoncho::start();
let mut c = clients(&fake);
fake.set(Mode::Status(409));
c.ensure_workspace(Workspace::Trusted).unwrap();
c.ensure_peer(Workspace::Trusted, "kyle").unwrap();
fake.clear();
c.ensure_workspace(Workspace::Trusted).unwrap();
assert!(fake.requests().is_empty());
}
#[test]
fn errors_by_kind_and_status() {
let fake = FakeHoncho::start();
let mut c = clients(&fake);
let cases = [
(
500,
HonchoError::Unreachable {
kind: "server",
status: Some(500),
},
),
(
503,
HonchoError::Unreachable {
kind: "server",
status: Some(503),
},
),
(401, HonchoError::KeyRefused { status: 401 }),
(403, HonchoError::KeyRefused { status: 403 }),
(
400,
HonchoError::Refused {
kind: "status",
status: Some(400),
},
),
(
404,
HonchoError::Refused {
kind: "status",
status: Some(404),
},
),
(
422,
HonchoError::Refused {
kind: "status",
status: Some(422),
},
),
];
for (status, expected) in cases {
fake.set(Mode::Status(status));
assert_eq!(
c.ensure_workspace(Workspace::Trusted),
Err(expected.clone())
);
assert_eq!(c.ensure_peer(Workspace::Fleet, "kyle"), Err(expected));
}
// Nothing failed is remembered: the next call asks again, and succeeds.
fake.set(Mode::Fixtures);
fake.clear();
c.ensure_workspace(Workspace::Trusted).unwrap();
assert_eq!(fake.requests().len(), 1);
fake.set(Mode::Hang);
assert_eq!(
c.ensure_peer(Workspace::Trusted, "ana_b"),
Err(HonchoError::Unreachable {
kind: "timeout",
status: None
})
);
}
#[test]
fn nothing_listening_is_unreachable() {
let port = {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
};
let (mut c, _) =
Clients::new(&config(&format!("http://127.0.0.1:{port}")), &no_env, FAST).unwrap();
assert_eq!(
c.ensure_workspace(Workspace::Trusted),
Err(HonchoError::Unreachable {
kind: "connect",
status: None
})
);
}
#[test]
fn an_error_never_holds_the_response() {
let fake = FakeHoncho::start();
let mut c = clients(&fake);
fake.set(Mode::Status(422));
let err = c
.ensure_peer(Workspace::Trusted, "private-note-text")
.unwrap_err();
let body = fake.requests()[0].body.clone().unwrap();
assert_eq!(body["id"], "private-note-text", "the fake did not echo it");
for shown in [err.to_string(), format!("{err:?}")] {
assert!(!shown.contains("private-note-text"), "{shown}");
}
assert_eq!(
err.to_string(),
"Honcho refused the request (status, HTTP 422)"
);
assert_eq!((err.kind(), err.status()), ("status", Some(422)));
let lines = [
HonchoError::Unreachable {
kind: "timeout",
status: None,
}
.to_string(),
HonchoError::KeyRefused { status: 401 }.to_string(),
];
assert_eq!(lines[0], "Honcho is unreachable (timeout)");
assert_eq!(lines[1], "Honcho refused the key (key-refused, HTTP 401)");
}
#[test]
fn every_roncho_error_is_classified() {
use roncho::blocking::Error;
let io = || std::io::Error::other("x");
let cases = [
(Error::Connect(io()), "connect", None, true),
(Error::Timeout, "timeout", None, true),
(Error::Closed, "closed", None, true),
(Error::Io(io()), "io", None, true),
(
Error::Status {
status: 502,
body: "b".into(),
},
"server",
Some(502),
true,
),
(
Error::Status {
status: 499,
body: "b".into(),
},
"status",
Some(499),
false,
),
(
Error::Redirect { status: 301 },
"redirect",
Some(301),
false,
),
(
Error::ActiveSessions { body: "b".into() },
"active-sessions",
Some(409),
false,
),
(Error::TooLarge, "too-large", None, false),
(Error::Decode("b".into()), "decode", None, false),
(Error::Stream("b".into()), "stream", None, false),
(Error::Config("b".into()), "config", None, false),
];
for (error, kind, status, unreachable) in cases {
let got = classify(&error);
assert_eq!((got.kind(), got.status()), (kind, status), "{error:?}");
assert_eq!(
matches!(got, HonchoError::Unreachable { .. }),
unreachable,
"{error:?}"
);
}
let got = classify(&Error::Unauthorized { status: 403 });
assert_eq!(got, HonchoError::KeyRefused { status: 403 });
}
#[test]
fn a_conclusion_deleted_or_gone_is_done() {
let fake = FakeHoncho::start();
let c = clients(&fake);
c.delete_conclusion(Workspace::Trusted, "YUBMqIqHhuJprV4o-inrg")
.unwrap();
c.delete_conclusion(Workspace::Trusted, "YUBMqIqHhuJprV4o-inrg")
.unwrap();
let requests = fake.requests();
assert_eq!(requests.len(), 2);
assert!(requests.iter().all(|r| r.method == "DELETE"
&& r.path == "/v3/workspaces/boxmaker0-trusted/conclusions/YUBMqIqHhuJprV4o-inrg"));
fake.set(Mode::Status(500));
assert_eq!(
c.delete_conclusion(Workspace::Trusted, "x"),
Err(HonchoError::Unreachable {
kind: "server",
status: Some(500)
})
);
fake.set(Mode::Status(403));
assert_eq!(
c.delete_conclusion(Workspace::Trusted, "x"),
Err(HonchoError::KeyRefused { status: 403 })
);
}