use super::*;
fn masked(text: &str) -> String {
return mask_userinfo_text(text);
}
#[test]
fn the_incident_line_loses_its_user_and_token() {
assert_eq!(
masked("batch response: Fatal error: Server error https://user:secret@lfs.example.invalid/objects/batch from HTTP 503"),
format!("batch response: Fatal error: Server error https://{CREDENTIALS_MASK}@lfs.example.invalid/objects/batch from HTTP 503"),
);
}
#[test]
fn a_user_alone_and_a_token_alone_are_masked() {
for userinfo in ["git", "ghp_token", "user:", ":pass", "us.er:p-a_s~s"] {
assert_eq!(masked(&format!("ssh://{userinfo}@host/r.git")), format!("ssh://{CREDENTIALS_MASK}@host/r.git"), "{userinfo}");
}
}
#[test]
fn an_at_sign_outside_the_authority_is_left_alone() {
for text in [
"https://host/path@name/objects",
"https://host/a/b@c@d",
"https://host?next=user@example.invalid",
"https://host#frag@ment",
"https://host:8443/@scope/pkg",
"https://[::1]:8080/@scope",
"https://host\\path@name",
"https://host",
"no url here, only user@example.invalid",
"scp-style git@host:path.git",
] {
assert_eq!(masked(text), text);
}
}
#[test]
fn the_userinfo_ends_at_the_last_at_sign_of_the_authority() {
assert_eq!(masked("https://user:p@ss@host/x"), format!("https://{CREDENTIALS_MASK}@host/x"));
assert_eq!(masked("https://a@b@c@host"), format!("https://{CREDENTIALS_MASK}@host"));
assert_eq!(masked("https://user:p@ss@host/path@tail"), format!("https://{CREDENTIALS_MASK}@host/path@tail"));
}
#[test]
fn percent_encoded_userinfo_is_masked_whole_and_the_path_keeps_its_encoding() {
assert_eq!(
masked("https://us%40er:p%3Ass%2F@host/a%40b/objects"),
format!("https://{CREDENTIALS_MASK}@host/a%40b/objects"),
);
assert_eq!(masked("https://%75ser:%73ecret@host/"), format!("https://{CREDENTIALS_MASK}@host/"));
}
#[test]
fn bracketed_ipv6_hosts_are_kept_and_their_userinfo_is_masked() {
assert_eq!(masked("https://user:secret@[::1]:8080/objects"), format!("https://{CREDENTIALS_MASK}@[::1]:8080/objects"));
assert_eq!(masked("https://u:s@[2001:db8::7]/x"), format!("https://{CREDENTIALS_MASK}@[2001:db8::7]/x"));
assert_eq!(masked("https://[fe80::1%25eth0]:9/x@y"), "https://[fe80::1%25eth0]:9/x@y");
}
#[test]
fn several_urls_on_one_line_are_masked_one_by_one() {
assert_eq!(
masked("from https://a:1@one.invalid/x to \"http://b:2@two.invalid:81/y\" via <ssh://c@three.invalid> (https://four.invalid/z@q)"),
format!(
"from https://{m}@one.invalid/x to \"http://{m}@two.invalid:81/y\" via <ssh://{m}@three.invalid> (https://four.invalid/z@q)",
m = CREDENTIALS_MASK
),
);
assert_eq!(
masked("https://a:1@h1/x,https://b:2@h2/y"),
format!("https://{m}@h1/x,https://{m}@h2/y", m = CREDENTIALS_MASK),
);
assert_eq!(
masked("Put \"https://u:p@storage.invalid/o?sig=a@b\": EOF; Post https://u:p@lfs.invalid/objects/batch: dial tcp"),
format!("Put \"https://{m}@storage.invalid/o?sig=a@b\": EOF; Post https://{m}@lfs.invalid/objects/batch: dial tcp", m = CREDENTIALS_MASK),
);
}
#[test]
fn the_authority_may_end_at_a_quote_a_bracket_or_the_end_of_the_text() {
for (before, after) in [("'", "'"), ("`", "`"), ("<", ">"), ("\"", "\""), ("", "")] {
assert_eq!(
masked(&format!("{before}https://u:p@host{after}")),
format!("{before}https://{CREDENTIALS_MASK}@host{after}"),
"{before}{after}"
);
}
}
#[test]
fn any_scheme_counts_and_a_bare_separator_does_not() {
assert_eq!(masked("git+ssh://u:p@host/r"), format!("git+ssh://{CREDENTIALS_MASK}@host/r"));
assert_eq!(masked("x-y.z1://u:p@host/r"), format!("x-y.z1://{CREDENTIALS_MASK}@host/r"));
assert_eq!(masked("file:///home/u@x/repo"), "file:///home/u@x/repo");
assert_eq!(masked(" ://u:p@host/r"), " ://u:p@host/r");
assert_eq!(masked("://u:p@host"), "://u:p@host");
}
#[test]
fn other_text_and_other_bytes_pass_through_unchanged() {
assert_eq!(masked(""), "");
assert_eq!(masked("Uploading LFS objects: 100% (3/3), 12 MB | 1.1 MB/s\r"), "Uploading LFS objects: 100% (3/3), 12 MB | 1.1 MB/s\r");
let raw: Vec<u8> = b"bad \xff\xfe bytes https://\xc3\xa9:p\xff@host/\xff tail".to_vec();
let expected: Vec<u8> = [b"bad \xff\xfe bytes https://".as_slice(), CREDENTIALS_MASK.as_bytes(), b"@host/\xff tail".as_slice()].concat();
assert_eq!(mask_userinfo(raw.as_slice()), expected);
}
#[test]
fn a_long_line_is_masked_in_linear_time() {
let line: String = "https://a@h ".repeat(200_000);
let started = std::time::Instant::now();
let result: String = masked(&line);
assert!(started.elapsed() < std::time::Duration::from_secs(30), "a quadratic scan of this line takes minutes: {:?}", started.elapsed());
assert!(!result.contains("//a@"), "every userinfo is hidden");
let unterminated: String = format!("https://{}", "@".repeat(1_000_000));
assert!(masked(&unterminated).starts_with("https://<credentials>@"));
}