use std::sync::Mutex;
use lys_pass::Error;
use super::{Door, Issuer, SignedIn, Started};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const NOW: u64 = 1_000;
struct Lys {
reachable: Mutex<bool>,
refreshed: Mutex<u32>,
}
impl Lys {
const fn new() -> Self {
Self {
reachable: Mutex::new(true),
refreshed: Mutex::new(0),
}
}
fn cut(&self) -> Result<(), Box<dyn std::error::Error>> {
*self.reachable.lock().map_err(|_| "poisoned")? = false;
Ok(())
}
}
impl Issuer for Lys {
fn start(&self) -> Result<Started, Error> {
Ok(Started {
url: "https://lys.example/oauth/authorize?state=s1".to_owned(),
state: "s1".to_owned(),
verifier: "v1".to_owned(),
browser: "b1".to_owned(),
})
}
fn finish(
&self,
code: &str,
_state: &str,
browser: &str,
started: &Started,
) -> Result<SignedIn, Error> {
if browser != started.browser {
return Err(Error::Invalid("the state was started in another browser"));
}
Ok(SignedIn {
token: format!("pass-for-{code}"),
holder: "clinician".to_owned(),
expires_at: NOW + 600,
refresh: Some("refresh-1".to_owned()),
})
}
fn refresh(&self, _refresh: &str) -> Result<SignedIn, Error> {
if !*self
.reachable
.lock()
.map_err(|_| Error::CannotAsk("poisoned"))?
{
return Err(Error::CannotAsk("the double is unreachable"));
}
let mut count = self
.refreshed
.lock()
.map_err(|_| Error::CannotAsk("poisoned"))?;
*count += 1;
let token = format!("refreshed-{count}");
drop(count);
Ok(SignedIn {
token,
holder: "clinician".to_owned(),
expires_at: NOW + 1_200,
refresh: Some("refresh-2".to_owned()),
})
}
}
#[test]
fn a_sign_in_keeps_the_pass_server_side() -> TestResult {
let door = Door::new(Lys::new());
let begun = door.begin(NOW)?;
assert_eq!(begun.browser, "b1");
let session = door.callback("c1", "s1", "b1", NOW)?;
assert!(!session.contains("pass"), "the session id carries the pass");
assert_eq!(session.len(), 64);
assert_eq!(door.pass(&session, NOW)?, "pass-for-c1");
Ok(())
}
#[test]
fn a_state_is_used_once_and_only_by_its_browser() -> TestResult {
let door = Door::new(Lys::new());
door.begin(NOW)?;
let other = door
.callback("c1", "s1", "b2", NOW)
.err()
.ok_or("another browser finished the sign-in")?;
assert_eq!(other.code, "contract_refused");
let again = door
.callback("c1", "s1", "b1", NOW)
.err()
.ok_or("a state was used twice")?;
assert_eq!(again.code, "sign_in_state_unknown");
Ok(())
}
#[test]
fn a_pass_is_refreshed_before_it_expires_and_refused_once_lys_cannot_be_asked() -> TestResult {
let lys = Lys::new();
let door = Door::new(lys);
door.begin(NOW)?;
let session = door.callback("c1", "s1", "b1", NOW)?;
assert_eq!(door.pass(&session, NOW + 100)?, "pass-for-c1");
assert_eq!(door.pass(&session, NOW + 590)?, "refreshed-1");
door.issuer().cut()?;
assert_eq!(door.pass(&session, NOW + 1_190)?, "refreshed-1");
let refused = door
.pass(&session, NOW + 1_200)
.err()
.ok_or("an expired pass was carried")?;
assert_eq!(refused.code, "lys_could_not_be_asked");
Ok(())
}
#[test]
fn a_signed_out_session_is_forgotten() -> TestResult {
let door = Door::new(Lys::new());
door.begin(NOW)?;
let session = door.callback("c1", "s1", "b1", NOW)?;
assert!(door.sign_out(&session)?, "the session was not held");
let refused = door
.pass(&session, NOW)
.err()
.ok_or("a signed-out session still carried its pass")?;
assert_eq!(refused.code, "session_unknown");
assert!(!door.sign_out(&session)?, "a session was forgotten twice");
Ok(())
}
#[test]
fn an_unknown_session_is_refused() {
let door = Door::new(Lys::new());
let refused = door.pass("nobody", NOW).err();
assert_eq!(
refused.map(|failure| failure.code),
Some("session_unknown".to_owned())
);
}