use super::*;
#[derive(serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Verdict {
Accepted,
Refused,
}
#[derive(serde::Deserialize)]
pub(crate) struct Spelling {
pub(crate) address: String,
pub(crate) verdict: Verdict,
}
pub(crate) static TABLE: std::sync::LazyLock<Vec<Spelling>> = std::sync::LazyLock::new(|| {
include_str!("../../../../tools/fixtures/address.jsonl")
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect()
});
#[test]
fn every_spelling_in_the_table_gets_its_verdict() {
for Spelling { address, verdict } in TABLE.iter() {
match verdict {
Verdict::Accepted => {
let addr = Address::parse(address).unwrap();
assert_eq!(addr.as_str(), address);
assert_eq!(addr.to_string(), *address);
}
Verdict::Refused => {
let err = Address::parse(address).unwrap_err();
assert_eq!(
err.code(),
&AxCode::InvalidArgs,
"should reject {address:?}"
);
}
}
}
}
#[test]
fn is_within_respects_segment_boundaries() {
let a = Address::parse("a/b/c").unwrap();
let prefix = Address::parse("a/b").unwrap();
let stranger = Address::parse("a/bc").unwrap();
assert!(a.is_within(&prefix));
assert!(a.is_within(&a));
assert!(!stranger.is_within(&prefix));
assert!(!prefix.is_within(&a));
}
#[test]
fn a_session_name_is_one_segment_a_person_typed() {
let named = SessionName::parse(" refactor the ledger ").unwrap();
assert_eq!(
named.as_str(),
"refactor the ledger",
"the ends are trimmed and the middle is theirs"
);
for wrong in [
"",
" ",
"lab/room1",
".",
"..",
".sprawling",
".git",
".GIT",
"a\\b",
"c:name",
] {
let err = SessionName::parse(wrong).unwrap_err();
assert_eq!(err.code(), &AxCode::InvalidArgs, "{wrong:?} was accepted");
assert!(!err.recovery().is_empty(), "{wrong:?}");
}
assert!(SessionName::parse(&"x".repeat(65)).is_err());
assert!(SessionName::parse(&"x".repeat(64)).is_ok());
}
#[test]
fn a_refused_address_names_the_first_rule_it_broke() {
for (raw, rule) in [
("", "address is empty"),
("/a", "address is absolute"),
("a\\b", "address contains a backslash"),
("c:a", "address contains `:`"),
("a\u{7}", "address contains a control character"),
("a//b", "address contains an empty segment"),
("a/.", "address contains a `.` or `..` segment"),
("a/../b", "address contains a `.` or `..` segment"),
("a./b", "a segment ends with a dot or whitespace"),
("a /b", "a segment ends with a dot or whitespace"),
] {
let err = Address::parse(raw).unwrap_err();
assert_eq!(
(err.code(), err.action(), err.subject()),
(&AxCode::InvalidArgs, "parse address", raw)
);
assert!(
err.recovery().starts_with(rule),
"{raw:?}: {}",
err.recovery()
);
}
}
#[test]
fn a_refused_session_name_names_the_first_rule_it_broke() {
for (raw, rule) in [
(" ", "a session with no name has no folder to work in"),
(".", "that names a directory rather than a session"),
(" .. ", "that names a directory rather than a session"),
(".Git", "that name belongs to the city itself"),
(
"a/b",
"a session name is one segment that stands on its own",
),
("a.", "a session name is one segment that stands on its own"),
] {
let err = SessionName::parse(raw).unwrap_err();
assert_eq!(
(err.code(), err.action(), err.subject()),
(&AxCode::InvalidArgs, "name a session", raw)
);
assert!(
err.recovery().starts_with(rule),
"{raw:?}: {}",
err.recovery()
);
}
}
#[test]
fn a_name_is_the_last_segment_and_a_session_name_displays_as_parsed() {
for (raw, name) in [
("solo", "solo"),
("lab/room1", "room1"),
("a/b/notes.md", "notes.md"),
] {
assert_eq!(Address::parse(raw).unwrap().name(), name);
}
let session = SessionName::parse(" ship it ").unwrap();
assert_eq!(session.to_string(), "ship it");
assert_eq!(SessionName::parse(&session.to_string()).unwrap(), session);
}
#[test]
fn a_reserved_subtree_is_reserved_at_whatever_depth_it_sits() {
assert!(Address::parse(".sprawling").unwrap().is_reserved());
assert!(Address::parse(".sprawling/ledger/x").unwrap().is_reserved());
assert!(!Address::parse(".sprawlingx/a").unwrap().is_reserved());
assert!(Address::parse("lab/.sprawling").unwrap().is_reserved());
assert!(
Address::parse("lab/.sprawling/CONFIG.toml")
.unwrap()
.is_reserved()
);
assert!(
Address::parse("lab/room1/.sprawling/CONFIG.toml")
.unwrap()
.is_reserved()
);
assert!(
!Address::parse("lab/sprawling/notes.md")
.unwrap()
.is_reserved(),
"a segment that merely looks like it is not it"
);
for alias in [".SPRAWLING", ".Sprawling/ledger", "lab/.SpRaWlInG/C.toml"] {
assert!(Address::parse(alias).unwrap().is_reserved(), "{alias}");
}
let upper = Address::parse(".SPRAWLING").unwrap();
let lower = Address::parse(".sprawling").unwrap();
assert_ne!(
upper, lower,
"identity stays byte-wise; only the guard folds"
);
}
#[test]
fn protected_metadata_is_reserved_at_whatever_depth_it_sits() {
for reserved in [
".git",
".git/hooks/pre-run",
"lab/.git/config",
".GIT/x",
"lab/.GiT/y",
] {
assert!(
Address::parse(reserved).unwrap().is_reserved(),
"{reserved}"
);
}
for open in [
"git/config",
"lab/github/notes.md",
"a.git/x",
"git.md",
".gits/x",
"lab/.gitlab/notes.md",
] {
assert!(
!Address::parse(open).unwrap().is_reserved(),
"{open} is a name that merely looks like it"
);
}
}
#[test]
fn the_read_bound_opens_home_and_ordinary_buildings_and_closes_confidential_ones() {
let rules_of = |target: &Address| -> Result<bool, AxError> {
match target.as_str().split('/').next() {
Some("vault" | "safe") => Ok(true),
Some("lab" | "hall") => Ok(false),
_ => Err(AxError::failure(
AxCode::ConfigInvalid,
"read a building's rules",
target.as_str(),
)
.with_recovery("fix the building's rules")),
}
};
let cells = [
("lab", "lab/room1/Memo.md", ReadVerdict::Open),
("lab", "hall/Roadmap.md", ReadVerdict::Open),
("lab", "vault/room1/notes.md", ReadVerdict::Confidential),
("vault", "vault/room1/notes.md", ReadVerdict::Open),
("vault", "lab/room1/Memo.md", ReadVerdict::Open),
("vault", "safe/keys.md", ReadVerdict::Confidential),
];
for (reader, target, expected) in cells {
let (reader, target) = (
Address::parse(reader).unwrap(),
Address::parse(target).unwrap(),
);
let verdict = may_read(&reader, &target, || {
assert!(
!target.is_within(&reader),
"{target} is at home and its rules were read"
);
rules_of(&target)
});
assert_eq!(verdict, expected, "{reader} reading {target}");
}
let broken = Address::parse("annex/plan.md").unwrap();
let verdict = may_read(&Address::parse("lab").unwrap(), &broken, || {
rules_of(&broken)
});
assert_eq!(
verdict,
ReadVerdict::RulesUnreadable(rules_of(&broken).unwrap_err()),
"rules that do not read close the building and keep the reason"
);
}
#[test]
fn deserialize_revalidates() {
let ok: Address = serde_json::from_str("\"a/b\"").unwrap();
assert_eq!(ok.as_str(), "a/b");
assert!(serde_json::from_str::<Address>("\"../up\"").is_err());
}
proptest::proptest! {
#[test]
fn accepted_addresses_roundtrip_and_never_contain_banned_parts(
segs in proptest::collection::vec("[a-zA-Z0-9._@-]{1,8}", 1..5)
) {
let raw = segs.join("/");
if let Ok(addr) = Address::parse(&raw) {
proptest::prop_assert_eq!(addr.as_str(), raw.as_str());
proptest::prop_assert!(!raw.split('/').any(|s| s.is_empty() || s == "." || s == ".."));
proptest::prop_assert!(
!raw.split('/').any(|s| s.ends_with('.') || s.ends_with(char::is_whitespace))
);
}
}
#[test]
fn a_windows_alias_of_protected_metadata_is_never_writable(
depth in "(lab/){0,2}",
name in "[.]([sS][pP][rR][aA][wW][lL][iI][nN][gG]|[gG][iI][tT])",
trailing in "[. ]{0,3}",
) {
let raw = format!("{depth}{name}{trailing}/CONFIG.toml");
let slipped = matches!(Address::parse(&raw), Ok(addr) if !addr.is_reserved());
proptest::prop_assert!(!slipped, "{raw} reaches a protected subtree");
}
#[test]
fn is_within_agrees_with_string_prefix_plus_boundary(
a in "[a-z]{1,3}(/[a-z]{1,3}){0,3}",
b in "[a-z]{1,3}(/[a-z]{1,3}){0,3}",
) {
let x = Address::parse(&a).unwrap();
let y = Address::parse(&b).unwrap();
let expect = a == b || a.starts_with(&format!("{b}/"));
proptest::prop_assert_eq!(x.is_within(&y), expect);
}
}