//! `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 { 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 }) ); }