#![cfg(feature = "git-lib")]
use pmat::services::git_clone::{CloneError, GitCloner};
use tempfile::TempDir;
#[test]
fn test_github_url_parsing_comprehensive() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let valid_cases = vec![
("https://github.com/rust-lang/rust", "rust-lang", "rust"),
("https://github.com/rust-lang/rust.git", "rust-lang", "rust"),
(
"https://github.com/USER-NAME/REPO-NAME",
"USER-NAME",
"REPO-NAME",
),
(
"https://github.com/user_name/repo_name",
"user_name",
"repo_name",
),
("https://github.com/user123/repo456", "user123", "repo456"),
("https://github.com/123/456", "123", "456"),
("https://github.com/a/b", "a", "b"),
("https://github.com/A/B", "A", "B"),
("git@github.com:rust-lang/rust.git", "rust-lang", "rust"),
("git@github.com:rust-lang/rust", "rust-lang", "rust"),
("git@github.com:USER/REPO.git", "USER", "REPO"),
(
"git@github.com:user-name/repo-name.git",
"user-name",
"repo-name",
),
("rust-lang/rust", "rust-lang", "rust"),
("user/repo", "user", "repo"),
("USER/REPO", "USER", "REPO"),
("user-name/repo-name", "user-name", "repo-name"),
("user_name/repo_name", "user_name", "repo_name"),
("123/456", "123", "456"),
];
for (url, expected_owner, expected_repo) in valid_cases {
match cloner.parse_github_url(url) {
Ok(parsed) => {
assert_eq!(
parsed.owner, expected_owner,
"Owner mismatch for URL: {url}"
);
assert_eq!(parsed.repo, expected_repo, "Repo mismatch for URL: {url}");
}
Err(e) => {
panic!("Failed to parse valid URL '{url}': {e:?}");
}
}
}
}
#[test]
fn test_github_url_parsing_invalid() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let invalid_cases = vec![
"",
" ",
"\t",
"\n",
" ",
"https://",
"https://github.com",
"https://github.com/",
"https://github.com/owner",
"https://github.com/owner/",
"git@github.com",
"git@github.com:",
"https://gitlab.com/owner/repo",
"https://bitbucket.org/owner/repo",
"https://example.com/owner/repo",
"http://github.com/owner/repo", "ftp://github.com/owner/repo",
"file:///etc/passwd",
"https://github.com/../repo",
"https://github.com/owner/../../../etc/passwd",
"https://github.com/.git/config",
"https://github.com/owner repo/name",
"https://github.com/owner/repo name",
"https://github.com/owner<script>/repo",
"https://github.com/owner/repo;rm -rf /",
"https://github.com//owner/repo",
"https://github.com/owner//repo",
"https://github.com/owner/repo//",
"https://github.com/owner/repo/extra",
"https://github.com/owner/repo/tree/main",
"https://github.com/owner/repo/blob/main/README.md",
];
for url in invalid_cases {
match cloner.parse_github_url(url) {
Ok(parsed) => {
panic!("Expected error for invalid URL '{url}', but got: {parsed:?}");
}
Err(CloneError::InvalidUrl(_)) => {
}
Err(e) => {
panic!("Expected InvalidUrl error for '{url}', but got: {e:?}");
}
}
}
}
#[test]
fn test_cache_key_generation() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let test_cases = vec![
("https://github.com/rust-lang/rust.git", true),
("git@github.com:rust-lang/rust.git", true),
("rust-lang/rust", true),
("https://github.com/user/repo", true),
("https://github.com/USER/REPO", true),
("https://github.com/user-name/repo-name", true),
("https://github.com/user_name/repo_name", true),
("https://github.com/123/456", true),
("special!@#$%^&*()chars", true),
("../../../etc/passwd", true),
("path/with/many/slashes", true),
("https://github.com/owner/repo?query=param", true),
("https://github.com/owner/repo#fragment", true),
];
for (url, _) in test_cases {
let cache_key = cloner.compute_cache_key(url);
assert!(
!cache_key.is_empty(),
"Cache key should not be empty for URL: {url}"
);
assert!(
cache_key
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-'),
"Cache key contains invalid characters for URL: {url}. Key: {cache_key}"
);
assert!(
!cache_key.contains('/'),
"Cache key contains '/' for URL: {url}. Key: {cache_key}"
);
assert!(
!cache_key.contains('\\'),
"Cache key contains '\\' for URL: {url}. Key: {cache_key}"
);
assert!(
!cache_key.contains(':'),
"Cache key contains ':' for URL: {url}. Key: {cache_key}"
);
assert!(
!cache_key.contains('.'),
"Cache key contains '.' for URL: {url}. Key: {cache_key}"
);
}
}
#[test]
fn test_cache_key_uniqueness() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let urls = vec![
"https://github.com/rust-lang/rust",
"https://github.com/rust-lang/cargo",
"https://github.com/tokio-rs/tokio",
"https://github.com/serde-rs/serde",
"git@github.com:rust-lang/rust.git",
"rust-lang/rust",
"RUST-LANG/RUST", ];
let mut cache_keys = std::collections::HashSet::new();
for url in urls {
let cache_key = cloner.compute_cache_key(url);
assert!(
cache_keys.insert(cache_key.clone()),
"Duplicate cache key '{cache_key}' for URL: {url}"
);
}
}
#[test]
fn test_url_normalization() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let equivalent_urls = vec![
vec![
"https://github.com/rust-lang/rust",
"https://github.com/rust-lang/rust.git",
" https://github.com/rust-lang/rust ", "https://github.com/rust-lang/rust/", ],
vec![
"git@github.com:rust-lang/rust.git",
"git@github.com:rust-lang/rust",
" git@github.com:rust-lang/rust.git ",
],
];
for group in equivalent_urls {
let mut parsed_urls = Vec::new();
for url in group {
match cloner.parse_github_url(url) {
Ok(parsed) => parsed_urls.push(parsed),
Err(e) => panic!("Failed to parse URL '{url}': {e:?}"),
}
}
for i in 1..parsed_urls.len() {
assert_eq!(
parsed_urls[0], parsed_urls[i],
"URLs should parse to the same result"
);
}
}
}
#[test]
fn test_security_boundaries() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let security_cases = vec![
"https://github.com/../../../etc/passwd",
"https://github.com/owner/../../../",
"https://github.com/./././owner/repo",
"https://github.com/%2e%2e%2f%2e%2e%2f",
"https://evil.com/github.com/owner/repo",
"https://github.com.evil.com/owner/repo",
"https://github.com@evil.com/owner/repo",
"https://github.com/.git/config",
"https://github.com/.ssh/authorized_keys",
"https://github.com/.env",
"javascript:alert('xss')",
"data:text/html,<script>alert('xss')</script>",
"file:///etc/passwd",
];
for url in security_cases {
let result = cloner.parse_github_url(url);
assert!(
result.is_err(),
"Security-sensitive URL should fail to parse: {url}"
);
}
}
#[test]
fn test_fuzzer_identified_security_issues() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let fuzzer_cases = vec![
"..",
"../..",
"https://github.com/../repo",
"https://github.com/owner/..",
"git@github.com:../repo.git",
"git@github.com:owner/...git",
"https://github.com/.git/repo",
"https://github.com/owner/.git",
".git/repo",
"owner/.git",
"https://github.com/.hidden/repo",
"https://github.com/owner/repo.",
"https://github.com/./repo",
"https://github.com/owner/.",
"https://github.com/.gitignore/repo",
"https://github.com/owner/.gitmodules",
"https://github.com/.gitattributes/repo",
"https://github.com/%2e%2e/repo",
"https://github.com/owner%2f..%2f..%2fetc%2fpasswd",
"https://github.com/ /repo",
"https://github.com/\t/repo",
"https://github.com/\n/repo",
];
for url in fuzzer_cases {
let result = cloner.parse_github_url(url);
assert!(
result.is_err(),
"Fuzzer-identified security URL should fail to parse: {url}"
);
}
}
#[test]
fn test_edge_case_handling() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let long_owner = "a".repeat(1000);
let long_repo = "b".repeat(1000);
let long_url = format!("https://github.com/{long_owner}/{long_repo}");
let _ = cloner.parse_github_url(&long_url);
let unicode_urls = vec![
"https://github.com/用户/仓库",
"https://github.com/пользователь/репозиторий",
"https://github.com/👤/📦",
];
for url in unicode_urls {
let _ = cloner.parse_github_url(url); }
let empty_component_urls = vec![
"https://github.com//repo",
"https://github.com/owner/",
"https://github.com///",
"/",
"//",
];
for url in empty_component_urls {
let _ = cloner.parse_github_url(url); }
}
#[ignore] #[tokio::test]
async fn test_clone_timeout() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf())
.with_timeout(std::time::Duration::from_millis(1));
let result = cloner
.clone_or_update("https://github.com/torvalds/linux") .await;
match result {
Err(CloneError::Timeout) => {
}
Ok(_) => {
panic!("Expected timeout error");
}
Err(e) => {
println!("Got error: {e:?}");
}
}
}
#[test]
fn test_round_trip_parsing() {
let temp_dir = TempDir::new().unwrap();
let cloner = GitCloner::new(temp_dir.path().to_path_buf());
let test_cases = vec![
("rust-lang", "rust"),
("tokio-rs", "tokio"),
("user-name", "repo-name"),
("user_name", "repo_name"),
("123", "456"),
("a", "b"),
];
for (owner, repo) in test_cases {
let https_url = format!("https://github.com/{owner}/{repo}");
let ssh_url = format!("git@github.com:{owner}/{repo}.git");
let short_url = format!("{owner}/{repo}");
let https_parsed = cloner.parse_github_url(&https_url).unwrap();
let ssh_parsed = cloner.parse_github_url(&ssh_url).unwrap();
let short_parsed = cloner.parse_github_url(&short_url).unwrap();
assert_eq!(https_parsed, ssh_parsed);
assert_eq!(https_parsed, short_parsed);
assert_eq!(https_parsed.owner, owner);
assert_eq!(https_parsed.repo, repo);
}
}