use std::collections::BTreeMap;
use std::path::PathBuf;
use serde_json::{Value, json};
use super::{Local, NoLys, link};
use crate::door::http::Request;
use crate::door::packed::Packed;
use crate::door::route::{Site, answer};
use crate::door::signin::Door;
use crate::serve::contracts::Caller;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn site(origin: &str) -> Site {
Site {
origin: origin.into(),
files: PathBuf::from("/unused"),
stores: BTreeMap::new(),
timeout: std::time::Duration::from_secs(1),
packed: Packed::default(),
store_children: BTreeMap::new(),
}
}
fn get(path: &str, query: &str, cookie: Option<&str>) -> Request {
Request {
method: "GET".into(),
path: path.into(),
query: query.into(),
headers: cookie
.map(|cookie| vec![("cookie".into(), format!("haem_session={cookie}"))])
.unwrap_or_default(),
body: Vec::new(),
}
}
fn asked(
door: &Door<NoLys>,
site: &Site,
request: &Request,
) -> Result<String, Box<dyn std::error::Error>> {
let mut bytes = Vec::new();
answer(door, site, request, 1_000).write(&mut bytes)?;
Ok(String::from_utf8(bytes)?)
}
fn body(response: &str) -> Result<Value, Box<dyn std::error::Error>> {
let (_, body) = response.split_once("\r\n\r\n").ok_or("no body")?;
Ok(serde_json::from_str(body)?)
}
fn door() -> Result<(Door<NoLys>, String), Box<dyn std::error::Error>> {
let (local, key) = Local::new(Caller::local("tom")?)?;
Ok((Door::at_keyboard(NoLys, local), key))
}
#[test]
fn the_printed_key_opens_one_session_for_the_local_owner() -> TestResult {
let (door, key) = door()?;
assert_eq!(key.len(), 64);
assert!(key.bytes().all(|byte| byte.is_ascii_hexdigit()));
assert_eq!(
link("http://127.0.0.1:7443", &key),
format!("http://127.0.0.1:7443/local?key={key}")
);
let site = site("http://127.0.0.1:7443");
let opened = asked(&door, &site, &get("/local", &format!("key={key}"), None))?;
assert!(opened.starts_with("HTTP/1.1 303 "), "{opened}");
assert!(opened.contains("\r\nLocation: /\r\n"), "{opened}");
let cookie = opened
.lines()
.find_map(|line| line.strip_prefix("Set-Cookie: haem_session="))
.ok_or("no session cookie")?;
let (id, flags) = cookie.split_once(';').ok_or("no cookie flags")?;
assert_eq!(flags, " HttpOnly; SameSite=Strict; Path=/");
let session = asked(&door, &site, &get("/api/session", "", Some(id)))?;
assert_eq!(body(&session)?, json!({"holder": "tom", "local": true}));
Ok(())
}
#[test]
fn an_https_origin_marks_the_cookie_secure() -> TestResult {
let (door, key) = door()?;
let opened = asked(
&door,
&site("https://localhost:7443"),
&get("/local", &format!("key={key}"), None),
)?;
assert!(
opened.contains("Set-Cookie: haem_session=")
&& opened.contains("; HttpOnly; Secure; SameSite=Strict; Path=/\r\n"),
"{opened}"
);
Ok(())
}
#[test]
fn a_spent_a_wrong_and_no_key_are_refused_in_the_same_words() -> TestResult {
let (door, key) = door()?;
let site = site("http://127.0.0.1:7443");
let wrong = asked(
&door,
&site,
&get("/local", &format!("key={}", "0".repeat(64)), None),
)?;
let none = asked(&door, &site, &get("/local", "", None))?;
asked(&door, &site, &get("/local", &format!("key={key}"), None))?;
let spent = asked(&door, &site, &get("/local", &format!("key={key}"), None))?;
for refused in [&wrong, &none, &spent] {
assert!(refused.starts_with("HTTP/1.1 401 "), "{refused}");
assert!(!refused.contains("Set-Cookie"), "{refused}");
assert_eq!(body(refused)?["code"], "door_local_key_refused");
}
assert_eq!(body(&wrong)?, body(&spent)?);
assert_eq!(body(&none)?, body(&spent)?);
Ok(())
}
#[test]
fn no_session_opens_nothing() -> TestResult {
let (door, _key) = door()?;
let site = site("http://127.0.0.1:7443");
let never_made = "0".repeat(64);
for cookie in [None, Some(never_made.as_str())] {
let stores = asked(&door, &site, &get("/api/haem", "", cookie))?;
assert_eq!(body(&stores)?["code"], "session_unknown", "{stores}");
let mut verb = get("/api/haem/wards", "", cookie);
verb.method = "POST".into();
verb.headers.push(("origin".into(), site.origin.clone()));
verb.body = br#"{"method":"branches.list"}"#.to_vec();
let refused = asked(&door, &site, &verb)?;
assert_eq!(body(&refused)?["code"], "session_unknown", "{refused}");
}
Ok(())
}
#[test]
fn a_door_with_no_lys_block_signs_nobody_in_through_lys_and_starts_no_assistant() -> TestResult {
let (door, key) = door()?;
let site = site("http://127.0.0.1:7443");
for path in ["/signin", "/callback"] {
let refused = asked(&door, &site, &get(path, "code=c&state=s", None))?;
assert_eq!(
body(&refused)?["code"],
"door_local_key_refused",
"{refused}"
);
}
let opened = asked(&door, &site, &get("/local", &format!("key={key}"), None))?;
let id = opened
.lines()
.find_map(|line| line.strip_prefix("Set-Cookie: haem_session="))
.and_then(|cookie| cookie.split(';').next())
.ok_or("no session cookie")?
.to_owned();
let mut start = get("/api/assistant/wards/start", "", Some(&id));
start.method = "POST".into();
start.headers.push(("origin".into(), site.origin.clone()));
let refused = asked(&door, &site, &start)?;
assert_eq!(body(&refused)?["code"], "assistant_needs_lys", "{refused}");
let events = asked(
&door,
&site,
&get("/api/assistant/wards/events", "", Some(&id)),
)?;
assert!(events.contains("text/event-stream"), "{events}");
assert!(events.contains("event: refused\ndata: "), "{events}");
assert!(events.contains("assistant_needs_lys"), "{events}");
let mut out = get("/signout", "", Some(&id));
out.method = "POST".into();
out.headers.push(("origin".into(), site.origin.clone()));
let signed_out = asked(&door, &site, &out)?;
assert!(signed_out.starts_with("HTTP/1.1 204 "), "{signed_out}");
assert!(
signed_out.contains(
"Set-Cookie: haem_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0\r\n"
),
"{signed_out}"
);
let after = asked(&door, &site, &get("/api/session", "", Some(&id)))?;
assert_eq!(body(&after)?["code"], "session_unknown", "{after}");
Ok(())
}