use crate::refusal::Refusal;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Allowed(String),
NamedRefusal(String),
PermissionRefusal(String),
}
#[derive(Debug, Clone)]
pub struct Case {
pub name: &'static str,
pub token: Option<&'static str>,
pub kind: &'static str,
pub id: &'static str,
pub action: &'static str,
pub expected: Outcome,
}
pub const ISSUER: &str = "https://issuer.example";
pub const AUDIENCE: &str = "sample";
pub const NOW: u64 = 100;
pub const KEYS: &str = include_str!("../fixtures/passes/keys.json");
pub const REFUSAL: &str = include_str!("../fixtures/refusals/missing-grant.json");
#[must_use]
pub fn cases() -> Vec<Case> {
let valid = include_str!("../fixtures/passes/valid.jwt").trim();
vec![
Case {
name: "valid",
token: Some(valid),
kind: "sample.file",
id: "child",
action: "read",
expected: Outcome::Allowed("parent-grant".to_owned()),
},
Case {
name: "wrong-audience",
token: Some(include_str!("../fixtures/passes/wrong-audience.jwt").trim()),
kind: "sample.file",
id: "child",
action: "read",
expected: Outcome::NamedRefusal("wrong_audience".to_owned()),
},
Case {
name: "expired",
token: Some(include_str!("../fixtures/passes/expired.jwt").trim()),
kind: "sample.file",
id: "child",
action: "read",
expected: Outcome::NamedRefusal("pass_expired".to_owned()),
},
Case {
name: "unpublished-key",
token: Some(include_str!("../fixtures/passes/unpublished-key.jwt").trim()),
kind: "sample.file",
id: "child",
action: "read",
expected: Outcome::NamedRefusal("unpublished_key".to_owned()),
},
Case {
name: "parent-reaches-child",
token: Some(valid),
kind: "sample.file",
id: "child",
action: "read",
expected: Outcome::Allowed("parent-grant".to_owned()),
},
Case {
name: "restricted-child",
token: Some(valid),
kind: "sample.file",
id: "restricted",
action: "read",
expected: Outcome::PermissionRefusal(REFUSAL.trim().to_owned()),
},
Case {
name: "held-mode-hot",
token: Some(valid),
kind: "sample.file",
id: "held",
action: "write",
expected: Outcome::PermissionRefusal(
include_str!("../fixtures/refusals/held-hot.json")
.trim()
.to_owned(),
),
},
Case {
name: "exact-refusal-bytes",
token: Some(valid),
kind: "sample.file",
id: "restricted",
action: "read",
expected: Outcome::PermissionRefusal(REFUSAL.trim().to_owned()),
},
Case {
name: "deliberate-unreachable",
token: None,
kind: "sample.file",
id: "child",
action: "read",
expected: Outcome::NamedRefusal("lys_could_not_be_asked".to_owned()),
},
Case {
name: "tampered",
token: Some(include_str!("../fixtures/passes/tampered.jwt").trim()),
kind: "sample.file",
id: "child",
action: "read",
expected: Outcome::NamedRefusal("signature_refused".to_owned()),
},
]
}
#[derive(Debug)]
pub struct Failure {
pub case: &'static str,
pub reason: String,
}
#[derive(Debug)]
pub struct Report {
pub run: usize,
pub passed: usize,
pub failures: Vec<Failure>,
}
impl Report {
pub fn require_conformant(&self) -> Result<(), Nonconformant> {
if self.passed == self.run && self.run == cases().len() && self.failures.is_empty() {
Ok(())
} else {
Err(Nonconformant {
run: self.run,
passed: self.passed,
})
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("lys_pass_nonconformance: {passed}/{run} vectors matched")]
pub struct Nonconformant {
pub run: usize,
pub passed: usize,
}
pub fn run<E: std::fmt::Display>(mut observe: impl FnMut(&Case) -> Result<Outcome, E>) -> Report {
let cases = cases();
let mut report = Report {
run: cases.len(),
passed: 0,
failures: Vec::new(),
};
for case in &cases {
match observe(case) {
Ok(actual) if actual == case.expected => report.passed += 1,
Ok(actual) => report.failures.push(Failure {
case: case.name,
reason: format!("expected {:?}, received {actual:?}", case.expected),
}),
Err(error) => report.failures.push(Failure {
case: case.name,
reason: error.to_string(),
}),
}
}
report
}
pub fn refusal_fixture() -> Result<Refusal, serde_json::Error> {
serde_json::from_str(REFUSAL)
}