use gunnar_core::sandbox::red::{ESCAPES, LEGITIMATE};
use gunnar_core::sandbox::{resolve_repo_path, MAX_COMPONENTS as SANDBOX_MAX_COMPONENTS};
use gunnar_proto::{RepoPath, RepoPathError, MAX_COMPONENTS};
#[test]
fn every_path_the_ssh_sandbox_accepts_is_expressible_as_a_repo_path() {
let nested = LEGITIMATE
.iter()
.filter(|(_, namespace, _)| namespace.contains('/'))
.count();
assert!(
nested >= 2,
"the sandbox's LEGITIMATE table has {nested} nested-namespace rows; with fewer than two \
this test would pass against a `RepoPath` that rejects nesting outright, which is the \
bug this is about"
);
for (input, namespace, name) in LEGITIMATE {
let resolved = resolve_repo_path(input).unwrap_or_else(|err| {
panic!("the sandbox no longer resolves its own row {input:?}: {err}")
});
assert_eq!(
(resolved.namespace(), resolved.name()),
(*namespace, *name),
"the sandbox resolves {input:?} somewhere else now"
);
RepoPath::new(*namespace, *name)
.validate()
.unwrap_or_else(|err| {
panic!(
"the sandbox serves {input:?} as {namespace}/{name}, but the control plane \
refuses to name it: {err}. Such a repository is clonable over SSH and \
invisible to gRPC."
)
});
if !input.starts_with('/') {
assert_eq!(
RepoPath::parse(input).unwrap_or_else(|err| panic!("{input:?} rejected: {err}")),
RepoPath::new(*namespace, *name),
"the SSH spelling and the gRPC spelling must name the same repository"
);
}
}
}
#[test]
fn the_component_bound_is_the_sandboxs_and_not_a_second_number() {
assert_eq!(
MAX_COMPONENTS, SANDBOX_MAX_COMPONENTS,
"gunnar-proto's component bound has drifted from the sandbox's; a path the sandbox \
accepts would be unnameable over gRPC, or one it refuses would be nameable"
);
}
#[test]
fn accepting_nesting_did_not_open_the_structural_escapes() {
for (input, expected) in [
("a/b/c/d/e", RepoPathError::TooManyComponents),
("ns//repo", RepoPathError::EmptyNamespace),
(
"ns/../../etc/passwd",
RepoPathError::IllegalCharacter("`.` or `..`"),
),
(
"ns/repo/../../../root",
RepoPathError::IllegalCharacter("`.` or `..`"),
),
(
"ns/repo/../other",
RepoPathError::IllegalCharacter("`.` or `..`"),
),
(
"ns/.git/../..",
RepoPathError::IllegalCharacter("`.` or `..`"),
),
(
"ns/..\\..\\etc",
RepoPathError::IllegalCharacter("a backslash"),
),
] {
assert!(
ESCAPES.iter().any(|(row, _)| *row == input),
"{input:?} is asserted here as an escape but is not in the sandbox's ESCAPES \
table. One of the two is wrong"
);
assert_eq!(
RepoPath::parse(input)
.map(|p| p.to_string())
.expect_err(&format!("{input:?} must be rejected")),
expected,
"wrong reason for {input:?}"
);
}
}
#[test]
fn a_nested_namespace_survives_the_split_and_the_suffix() {
let path = RepoPath::parse("team/sub/repo.git").expect("a nested namespace is legal over SSH");
assert_eq!(path.namespace, "team/sub");
assert_eq!(path.repo, "repo");
assert_eq!(path.to_string(), "team/sub/repo");
let deep = RepoPath::parse("team/sub/deeper/repo.git").expect("four components is the bound");
assert_eq!(deep.namespace, "team/sub/deeper");
assert_eq!(deep.repo, "repo");
}