use super::{Positional, classify, looks_like_repo, path_segments, resolve_register_args};
#[test]
fn classify_sorts_every_shape() {
for s in [
"https://github.com/owner/repo",
"git@github.com:owner/repo.git",
"ssh://git@git.example.com:2222/team/thing.git",
"github.com/owner/repo",
"localhost:3000/owner/repo",
"/srv/git/repo.git",
"~/src/repo.git",
] {
assert!(matches!(classify(s), Positional::Url(_)), "{s} not a URL");
}
assert_eq!(
classify("bobmatnyc/trusty-tools"),
Positional::Shorthand {
owner: "bobmatnyc",
repo: "trusty-tools"
}
);
assert_eq!(
classify("123/repo"),
Positional::Shorthand {
owner: "123",
repo: "repo"
}
);
assert_eq!(
classify("owner/repo.js"),
Positional::Shorthand {
owner: "owner",
repo: "repo.js"
}
);
for s in [
"./repo",
"../repo",
".",
"..",
".hidden/repo",
".config/x/y",
] {
assert_eq!(classify(s), Positional::RelativePath, "{s} misclassified");
}
for s in ["my-alias", "owner/", "a/b/c", "owner/re po", "owner:repo"] {
assert_eq!(classify(s), Positional::Other, "{s} misclassified");
}
}
#[test]
fn looks_like_repo_rejects_aliases() {
for alias in ["my-alias", "proj", "a.b-c", "repo123", "owner-repo"] {
assert!(!looks_like_repo(alias), "{alias} misread as a repo");
}
}
#[test]
fn shorthand_resolves_to_github() {
let (alias, url) = resolve_register_args("bobmatnyc/trusty-tools", None).unwrap();
assert_eq!(url, "https://github.com/bobmatnyc/trusty-tools");
assert_eq!(alias, "bobmatnyc-trusty-tools");
let (from_url, _) =
resolve_register_args("https://github.com/bobmatnyc/trusty-tools", None).unwrap();
assert_eq!(alias, from_url);
}
#[test]
fn shorthand_numeric_owner() {
let (alias, url) = resolve_register_args("123/repo", None).unwrap();
assert_eq!(url, "https://github.com/123/repo");
assert_eq!(alias, "123-repo");
}
#[test]
fn shorthand_with_explicit_alias() {
let (alias, url) = resolve_register_args("owner/repo", Some("shiny")).unwrap();
assert_eq!(alias, "shiny");
assert_eq!(url, "https://github.com/owner/repo");
let (alias, url) = resolve_register_args("shiny", Some("owner/repo")).unwrap();
assert_eq!(alias, "shiny");
assert_eq!(url, "https://github.com/owner/repo");
}
#[test]
fn relative_paths_are_never_shorthand() {
for s in [
"./repo",
"../repo",
"./owner/repo",
"../a/b",
".hidden/repo",
".github/workflows",
] {
let err = resolve_register_args(s, None).unwrap_err().to_string();
assert!(
err.contains("relative path"),
"{s} must be refused as a path, got: {err}"
);
}
}
#[test]
fn absolute_and_home_paths_are_accepted() {
let (alias, url) = resolve_register_args("/srv/git/repo.git", Some("local")).unwrap();
assert_eq!(alias, "local");
assert_eq!(url, "/srv/git/repo.git", "an absolute path is stored as-is");
assert!(resolve_register_args("~/src/repo.git", Some("home")).is_ok());
}
#[test]
fn home_relative_path_is_expanded() {
let home = dirs::home_dir().expect("home directory");
let (_, url) = resolve_register_args("~/src/repo.git", Some("home")).unwrap();
assert_eq!(url, home.join("src/repo.git").to_string_lossy());
assert!(!url.contains('~'), "tilde must not survive: {url}");
}
#[test]
fn two_args_url_first() {
let (alias, url) =
resolve_register_args("https://github.com/owner/repo", Some("my-alias")).unwrap();
assert_eq!(alias, "my-alias");
assert_eq!(url, "https://github.com/owner/repo");
}
#[test]
fn two_args_legacy_alias_first() {
let (alias, url) =
resolve_register_args("my-alias", Some("https://github.com/owner/repo")).unwrap();
assert_eq!(alias, "my-alias");
assert_eq!(url, "https://github.com/owner/repo");
}
#[test]
fn two_args_legacy_ssh_url_second() {
let (alias, url) =
resolve_register_args("proj", Some("git@github.com:owner/repo.git")).unwrap();
assert_eq!(alias, "proj");
assert_eq!(url, "git@github.com:owner/repo.git");
}
#[test]
fn one_arg_derives() {
let (alias, url) = resolve_register_args("https://github.com/owner/repo", None).unwrap();
assert_eq!(alias, "owner-repo");
assert_eq!(url, "https://github.com/owner/repo");
}
#[test]
fn one_arg_non_repo_errors() {
for s in ["my-alias", "owner/", "a/b/c"] {
let err = resolve_register_args(s, None).unwrap_err().to_string();
assert!(
err.contains("does not name a repository"),
"{s}: unexpected {err}"
);
assert!(err.contains("<owner>/<repo>"), "no remedy in: {err}");
}
}
#[test]
fn two_urls_error() {
for (a, b) in [
("https://github.com/a/b", "https://github.com/c/d"),
("owner/repo", "other/repo"),
] {
let err = resolve_register_args(a, Some(b)).unwrap_err().to_string();
assert!(
err.contains("both arguments name a repository"),
"({a}, {b}): unexpected {err}"
);
}
}
#[test]
fn two_non_urls_error() {
let err = resolve_register_args("alpha", Some("beta"))
.unwrap_err()
.to_string();
assert!(
err.contains("neither argument names a repository"),
"unexpected: {err}"
);
}
#[test]
fn derives_from_every_url_shape() {
let cases = [
("https://github.com/owner/repo", "owner-repo"),
("https://github.com/owner/repo.git", "owner-repo"),
("https://github.com/owner/repo/", "owner-repo"),
("https://github.com/owner/repo.git/", "owner-repo"),
("git@github.com:owner/repo.git", "owner-repo"),
("git@github.com:owner/repo", "owner-repo"),
("https://gitlab.com/acme/widget.git", "acme-widget"),
("https://git.example.com/team/thing", "team-thing"),
(
"ssh://git@git.example.com:2222/team/thing.git",
"team-thing",
),
("git@bitbucket.org:team/thing.git", "team-thing"),
];
for (url, want) in cases {
let (alias, got_url) = resolve_register_args(url, None).unwrap();
assert_eq!(alias, want, "wrong alias for {url}");
assert_eq!(got_url, url, "URL must be stored verbatim");
}
}
#[test]
fn scheme_less_host_gains_https() {
let cases = [
(
"github.com/owner/repo",
"https://github.com/owner/repo",
"owner-repo",
),
("a.b/c", "https://a.b/c", "c"),
("localhost/repo", "https://localhost/repo", "repo"),
];
for (input, want_url, want_alias) in cases {
let (alias, url) = resolve_register_args(input, None).unwrap();
assert_eq!(url, want_url, "for {input}");
assert_eq!(alias, want_alias, "for {input}");
}
}
#[test]
fn no_owner_falls_back_to_repo() {
let (alias, _) = resolve_register_args("https://example.com/repo", None).unwrap();
assert_eq!(alias, "repo");
}
#[test]
fn host_only_url_errors() {
for url in [
"https://example.com/",
"https://example.com",
"https://example.com:8080",
"git@github.com:",
] {
let err = resolve_register_args(url, None).unwrap_err().to_string();
assert!(
err.contains("no owner/repo path"),
"{url} must be refused, got: {err}"
);
}
}
#[test]
fn browser_paste_shapes_are_rejected() {
let cases = [
"https://github.com/owner/repo/tree/main",
"https://github.com/owner/repo/pull/4914",
"https://github.com/owner/repo/blob/main/README.md",
"https://github.com/owner/repo/issues/12",
"https://github.com/owner/repo/commit/abc123",
"https://gitlab.com/group/sub/repo/-/blob/main/x",
];
for url in cases {
let err = resolve_register_args(url, None).unwrap_err().to_string();
assert!(
err.contains("points inside a repository"),
"{url} must be refused, got: {err}"
);
assert!(err.contains("repository root"), "no remedy in: {err}");
}
}
#[test]
fn github_tab_urls_are_rejected() {
let cases = [
"https://github.com/owner/repo/issues",
"https://github.com/owner/repo/pulls",
"https://github.com/owner/repo/releases",
"https://github.com/owner/repo/wiki",
"https://github.com/owner/repo/actions",
"https://github.com/owner/repo/settings",
"github.com/owner/repo/issues",
"git@github.com:owner/repo/issues",
];
for url in cases {
let err = resolve_register_args(url, None).unwrap_err().to_string();
assert!(
err.contains("points inside a repository"),
"{url} must be refused, got: {err}"
);
assert!(err.contains("repository root"), "no remedy in: {err}");
}
}
#[test]
fn repo_named_like_a_web_path_is_ok() {
let (alias, _) = resolve_register_args("https://github.com/owner/tree", None).unwrap();
assert_eq!(alias, "owner-tree");
let (alias, _) = resolve_register_args("https://gitlab.com/group/sub/blob", None).unwrap();
assert_eq!(alias, "sub-blob");
}
#[test]
fn local_paths_skip_the_web_path_rule() {
for input in [
"/Users/me/actions/repo.git",
"/home/bob/raw/myrepo",
"/srv/git/tree/repo.git",
"file:///Users/me/actions/repo.git",
] {
resolve_register_args(input, Some("local"))
.unwrap_or_else(|e| panic!("{input} must register, got: {e}"));
}
let (_, url) = resolve_register_args("~/tree/repo.git", Some("home")).unwrap();
assert!(url.ends_with("/tree/repo.git"), "unexpected: {url}");
assert!(!url.contains('~'), "tilde must not survive: {url}");
}
#[test]
fn query_and_fragment_are_stripped() {
for input in [
"https://github.com/owner/repo?tab=readme-ov-file",
"https://github.com/owner/repo#readme",
] {
let (alias, url) = resolve_register_args(input, None).unwrap();
assert_eq!(alias, "owner-repo", "for {input}");
assert_eq!(url, "https://github.com/owner/repo", "for {input}");
}
}
#[test]
fn hyphen_flattening_ambiguity_is_accepted_and_collides() {
let (a, _) = resolve_register_args("a/b-c", None).unwrap();
let (b, _) = resolve_register_args("a-b/c", None).unwrap();
assert_eq!(a, "a-b-c");
assert_eq!(b, "a-b-c");
}
#[test]
fn path_segments_matches_every_url_shape() {
let cases: [(&str, Vec<&str>); 8] = [
("https://github.com/owner/repo", vec!["owner", "repo"]),
("https://github.com/owner/repo/", vec!["owner", "repo"]),
("github.com/owner/repo", vec!["owner", "repo"]),
("git@github.com:owner/repo.git", vec!["owner", "repo.git"]),
(
"ssh://git@git.example.com:2222/team/thing.git",
vec!["team", "thing.git"],
),
("https://example.com:8080/o/r", vec!["o", "r"]),
("https://example.com/repo", vec!["repo"]),
(
"https://github.com/o/r/tree/main",
vec!["o", "r", "tree", "main"],
),
];
for (url, want) in cases {
assert_eq!(path_segments(url), want, "wrong segments for {url}");
}
}
#[test]
fn path_segments_keeps_numeric_owner() {
assert_eq!(
path_segments("https://github.com/123/repo"),
["123", "repo"]
);
let (alias, _) = resolve_register_args("https://github.com/123/repo", None).unwrap();
assert_eq!(alias, "123-repo");
}
#[test]
fn path_segments_host_only() {
for url in ["https://example.com", "https://example.com/", "example.com"] {
assert!(
path_segments(url).is_empty(),
"{url} must yield no path segments"
);
}
}
#[test]
fn collision_refuses_and_leaves_registry_untouched() {
let dir = tempfile::TempDir::new().unwrap();
let paths = crate::commands::managed_root::ManagedPaths::from_root(dir.path().to_path_buf());
crate::commands::standalone::register_cmd(&paths, "a/b-c", None, false).unwrap();
let registry_path = dir.path().join("registry.json");
let before = std::fs::read(®istry_path).unwrap();
let err = crate::commands::standalone::register_cmd(&paths, "a-b/c", None, false).unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("already registered"),
"collision must be loud: {msg}"
);
let after = std::fs::read(®istry_path).unwrap();
assert_eq!(
before, after,
"a refused registration must not mutate the registry"
);
}
#[test]
fn rejected_url_writes_nothing() {
let dir = tempfile::TempDir::new().unwrap();
let paths = crate::commands::managed_root::ManagedPaths::from_root(dir.path().to_path_buf());
for bad in ["./repo", "https://example.com", "a/b/c"] {
assert!(
crate::commands::standalone::register_cmd(&paths, bad, None, false).is_err(),
"{bad} must be refused"
);
}
assert!(
!dir.path().join("registry.json").exists(),
"a refused argument must not write a registry"
);
}
#[test]
fn shorthand_registers_end_to_end() {
let dir = tempfile::TempDir::new().unwrap();
let paths = crate::commands::managed_root::ManagedPaths::from_root(dir.path().to_path_buf());
crate::commands::standalone::register_cmd(&paths, "bobmatnyc/trusty-tools", None, false)
.unwrap();
let registry =
trusty_mpm::core::standalone::registry::ManagedRegistry::load(dir.path()).unwrap();
let entries = registry.list();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].alias, "bobmatnyc-trusty-tools");
assert_eq!(entries[0].url, "https://github.com/bobmatnyc/trusty-tools");
}
#[test]
fn same_url_reregistration_is_idempotent() {
let dir = tempfile::TempDir::new().unwrap();
let paths = crate::commands::managed_root::ManagedPaths::from_root(dir.path().to_path_buf());
crate::commands::standalone::register_cmd(&paths, "owner/repo", None, false).unwrap();
crate::commands::standalone::register_cmd(&paths, "https://github.com/owner/repo", None, false)
.unwrap();
let registry =
trusty_mpm::core::standalone::registry::ManagedRegistry::load(dir.path()).unwrap();
let entries = registry.list();
assert_eq!(entries.len(), 1, "shorthand and full URL are one repo");
assert_eq!(entries[0].alias, "owner-repo");
}