use super::children::Children;
use super::config::Config;
use super::http;
use super::route;
use super::signin::{Door, Issuer, SignedIn, Started};
use lys_pass::Error;
use serde_json::json;
use std::io::Cursor;
type TestResult = Result<(), Box<dyn std::error::Error>>;
struct FixtureIssuer;
impl Issuer for FixtureIssuer {
fn start(&self) -> Result<Started, Error> {
Ok(Started {
url: "https://lys.example/authorize".into(),
state: "state".into(),
verifier: "verifier".into(),
browser: "browser".into(),
})
}
fn finish(&self, _: &str, _: &str, _: &str, _: &Started) -> Result<SignedIn, Error> {
Ok(SignedIn {
token: "fixture-only".into(),
holder: "fixture".into(),
expires_at: 1000,
refresh: None,
})
}
fn refresh(&self, _: &str) -> Result<SignedIn, Error> {
Err(Error::CannotAsk("fixture has no refresh"))
}
}
#[test]
fn r3_given_up_store_answers_503_while_other_routes_are_served() -> TestResult {
let directory = tempfile::Builder::new()
.prefix("door-route-")
.tempdir_in("/tmp")?;
let records = directory.path().join("starts");
let config = json!({"listen":"127.0.0.1:0", "origin":"https://studio.example", "files":directory.path(),
"stores":{"fixture":directory.path().join("absent.sock")},
"lys":{"issuer":"https://lys.example", "client_id":"fixture", "secret_file":directory.path().join("unused"), "redirect":"https://studio.example/callback"},
"children":[{"name":"broken-store", "program":"/usr/bin/python3", "args":["-u","-c",
"import os, sys\np=sys.argv[1]\nn=len(open(p).readlines())+1 if os.path.exists(p) else 1\nwith open(p, 'a') as f: f.write('started\\n')\nif n > 3: print('fixture ready unexpected extra start', file=sys.stderr, flush=True); sys.stdin.read()\nsys.exit(9)", records],
"ready":"fixture ready", "log":directory.path().join("child.log"), "store":"fixture", "give_up_after":3}]
});
let config = Config::parse(&serde_json::to_vec(&config)?)?;
let site = config.site();
let children = Children::start(&config.children)?;
let door = Door::new(FixtureIssuer);
let begun = door.begin(100)?;
let session = door.callback("code", "state", &begun.browser, 100)?;
let body = r#"{"method":"catalogue.list","params":{}}"#;
let request = format!(
"POST /api/haem/fixture HTTP/1.1\r\nOrigin: https://studio.example\r\nCookie: haem_session={session}\r\nContent-Length: {}\r\n\r\n{body}",
body.len()
);
let request = http::read(&mut Cursor::new(request), 16384, 65536)?;
let mut bytes = Vec::new();
route::answer(&door, &site, &request, 100).write(&mut bytes)?;
let response = String::from_utf8(bytes)?;
assert!(response.starts_with("HTTP/1.1 503 "), "{response}");
assert!(response.contains("store_child_given_up"), "{response}");
assert!(response.contains("broken-store"), "{response}");
let request = format!("GET /api/haem HTTP/1.1\r\nCookie: haem_session={session}\r\n\r\n");
let request = http::read(&mut Cursor::new(request), 16384, 65536)?;
let mut bytes = Vec::new();
route::answer(&door, &site, &request, 100).write(&mut bytes)?;
assert!(String::from_utf8(bytes)?.starts_with("HTTP/1.1 200 "));
drop(children);
Ok(())
}
fn failed_site(root: &std::path::Path) -> route::Site {
use std::collections::BTreeMap;
use std::sync::{Arc, atomic::AtomicBool};
route::Site {
origin: "https://studio.example".into(),
files: root.into(),
stores: BTreeMap::from([("fixture".into(), root.join("absent.sock"))]),
store_children: BTreeMap::from([(
"fixture".into(),
("broken-store".into(), Arc::new(AtomicBool::new(true))),
)]),
timeout: std::time::Duration::from_secs(1),
packed: super::packed::Packed::default(),
}
}
#[test]
fn review_3_assistant_store_given_up_is_503_after_session_check() -> TestResult {
let directory = tempfile::tempdir_in("/tmp")?;
let site = failed_site(directory.path());
let door = Door::new(FixtureIssuer);
let begun = door.begin(100)?;
let session = door.callback("code", "state", &begun.browser, 100)?;
for (cookie, expected) in [
(
format!("Cookie: haem_session={session}\r\n"),
"HTTP/1.1 503 ",
),
(String::new(), "HTTP/1.1 401 "),
] {
let text = format!(
"POST /api/assistant/fixture/start HTTP/1.1\r\nOrigin: https://studio.example\r\n{cookie}\r\n"
);
let request = http::read(&mut Cursor::new(text), 16_384, 65_536)?;
let mut response = Vec::new();
route::answer(&door, &site, &request, 100).write(&mut response)?;
let response = String::from_utf8(response)?;
assert!(response.starts_with(expected), "{response}");
if expected.contains("503") {
assert!(
response.contains("store_child_given_up") && response.contains("broken-store"),
"{response}"
);
}
}
Ok(())
}
#[test]
fn review_3_store_list_names_the_given_up_child() -> TestResult {
let directory = tempfile::tempdir_in("/tmp")?;
let site = failed_site(directory.path());
let door = Door::new(FixtureIssuer);
let begun = door.begin(100)?;
let session = door.callback("code", "state", &begun.browser, 100)?;
let request = http::read(
&mut Cursor::new(format!(
"GET /api/haem HTTP/1.1\r\nCookie: haem_session={session}\r\n\r\n"
)),
16_384,
65_536,
)?;
let mut bytes = Vec::new();
route::answer(&door, &site, &request, 100).write(&mut bytes)?;
let text = String::from_utf8(bytes)?;
let body: serde_json::Value =
serde_json::from_str(text.split_once("\r\n\r\n").ok_or("body missing")?.1)?;
assert_eq!(
body["stores"][0],
json!({"name":"fixture", "given_up":true, "child":"broken-store"})
);
Ok(())
}