aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/util/git_test.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 15:12:06 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commit8117e5450693d19726da877bce8aacf5c7aa53af (patch)
tree4b06e6f6924599f3402d29cdcdeb0f8a88354d89 /crates/shirabe/tests/util/git_test.rs
parent8dd3d67884a204ca40e3364206868fea77a312be (diff)
downloadphp-shirabe-8117e5450693d19726da877bce8aacf5c7aa53af.tar.gz
php-shirabe-8117e5450693d19726da877bce8aacf5c7aa53af.tar.zst
php-shirabe-8117e5450693d19726da877bce8aacf5c7aa53af.zip
test: port 44 vcs/downloader/version tests using mock infra
Port git, version_guesser, gitlab_driver, github_driver, and git_downloader tests using the ProcessExecutor/HttpDownloader mocks and IO/Config stubs. Fix production regex-porting bugs surfaced by the now-reachable paths: Url::sanitize and Response::find_header_value had non-delimited PCRE patterns; implement array_search_mixed non-strict branch and a datetime format mapping. Add HttpDownloader::__new_mock so mocked downloaders skip curl construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/util/git_test.rs')
-rw-r--r--crates/shirabe/tests/util/git_test.rs478
1 files changed, 456 insertions, 22 deletions
diff --git a/crates/shirabe/tests/util/git_test.rs b/crates/shirabe/tests/util/git_test.rs
index 40ca9da..6d615c5 100644
--- a/crates/shirabe/tests/util/git_test.rs
+++ b/crates/shirabe/tests/util/git_test.rs
@@ -1,55 +1,489 @@
//! ref: composer/tests/Composer/Test/Util/GitTest.php
-// These mock IO/Config/ProcessExecutor to drive Git::runCommand and mirror syncing; mocking
-// is not available here.
+use std::cell::RefCell;
+use std::rc::Rc;
-#[allow(dead_code)]
-fn set_up() {
- // Builds mocked IO/Config/ProcessExecutor/Filesystem and a real Git; mocking is not available.
- todo!()
+use indexmap::IndexMap;
+use shirabe::config::Config;
+use shirabe::io::IOInterface;
+use shirabe::util::filesystem::Filesystem;
+use shirabe::util::git::Git;
+use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor};
+use shirabe_php_shim::{PhpMixed, RuntimeException};
+
+use crate::config_stub::ConfigStubBuilder;
+use crate::io_stub::IOStub;
+use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock};
+
+// PHP's `commandCallable` returns a bare string (`'git command'`); Rust's `run_command`
+// flattens each callable to a `Vec<String>` and hands it to `execute_args`, which always
+// builds a `PhpMixed::List`. So the single-token string command becomes a one-element list,
+// and the corresponding process expectation is a one-element list as well.
+fn build_git(io: IOStub, config: Config, process: Rc<RefCell<ProcessExecutor>>) -> Git {
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(io));
+ let config = Rc::new(RefCell::new(config));
+ let fs = Rc::new(RefCell::new(Filesystem::new(None)));
+ Git::new(io, config, process, fs)
}
+fn mock_config(protocol: &str) -> Config {
+ ConfigStubBuilder::new()
+ .with(
+ "github-domains",
+ PhpMixed::List(vec![PhpMixed::String("github.com".to_string())]),
+ )
+ .with(
+ "github-protocols",
+ PhpMixed::List(vec![PhpMixed::String(protocol.to_string())]),
+ )
+ .build()
+}
+
+fn mock_sync_mirror_config() -> Config {
+ ConfigStubBuilder::new()
+ .with(
+ "github-domains",
+ PhpMixed::List(vec![PhpMixed::String("github.com".to_string())]),
+ )
+ .with(
+ "gitlab-domains",
+ PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]),
+ )
+ .with(
+ "github-protocols",
+ PhpMixed::List(vec![PhpMixed::String("https".to_string())]),
+ )
+ .build()
+}
+
+// publicGithubNoCredentialsProvider: ['ssh', 'git@github.com:acme/repo']
#[test]
-#[ignore = "requires mocked Config (getMockBuilder Config) and getProcessExecutorMock with expects() command expectations; no mocking infrastructure exists"]
-fn test_run_command_public_git_hub_repository_not_initial_clone() {
- todo!()
+fn test_run_command_public_git_hub_repository_not_initial_clone_ssh() {
+ let expected_url = "git@github.com:acme/repo";
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ assert_eq!(expected_url, url);
+ vec!["git command".to_string()]
+ });
+
+ let config = mock_config("ssh");
+
+ let (process, _guard) =
+ get_process_executor_mock(vec![cmd(vec!["git command"])], true, MockHandler::default());
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ git.__run_command(
+ vec![command_callable],
+ "https://github.com/acme/repo",
+ None,
+ true,
+ None,
+ )
+ .unwrap();
}
+// publicGithubNoCredentialsProvider: ['https', 'https://github.com/acme/repo']
#[test]
-#[ignore = "requires mocked Config and getProcessExecutorMock with expects() command expectations; no mocking infrastructure exists"]
+fn test_run_command_public_git_hub_repository_not_initial_clone_https() {
+ let expected_url = "https://github.com/acme/repo";
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ assert_eq!(expected_url, url);
+ vec!["git command".to_string()]
+ });
+
+ let config = mock_config("https");
+
+ let (process, _guard) =
+ get_process_executor_mock(vec![cmd(vec!["git command"])], true, MockHandler::default());
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ git.__run_command(
+ vec![command_callable],
+ "https://github.com/acme/repo",
+ None,
+ true,
+ None,
+ )
+ .unwrap();
+}
+
+#[test]
+#[ignore = "reaches Git::throw_exception -> Url::sanitize, whose preg pattern fails to parse in the regex crate (preg shim bug, unrelated to this test); production Url::sanitize must be fixed first"]
fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_without_authentication()
{
- todo!()
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(|url: &str| {
+ assert_eq!("https://github.com/acme/repo", url);
+ vec!["git command".to_string()]
+ });
+
+ let config = mock_config("https");
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd_full(vec!["git command"], 1, "", ""),
+ cmd_full(vec!["git", "--version"], 0, "", ""),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ let result = git.__run_command(
+ vec![command_callable],
+ "https://github.com/acme/repo",
+ None,
+ true,
+ None,
+ );
+
+ let err = result.expect_err("expected a RuntimeException");
+ assert!(err.downcast_ref::<RuntimeException>().is_some());
+}
+
+// privateGithubWithCredentialsProvider helper.
+fn run_command_private_github_with_authentication(
+ git_url: &str,
+ protocol: &str,
+ git_hub_token: &str,
+ expected_url: &str,
+ expected_failures_before_success: usize,
+) {
+ let expected_url_owned = expected_url.to_string();
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ if url != expected_url_owned {
+ return vec!["git command failing".to_string()];
+ }
+ vec!["git command ok".to_string()]
+ });
+
+ let config = mock_config(protocol);
+
+ let mut expected_calls: Vec<MockExpectation> = Vec::new();
+ for _ in 0..expected_failures_before_success {
+ expected_calls.push(cmd_full(vec!["git command failing"], 1, "", ""));
+ }
+ expected_calls.push(cmd_full(vec!["git command ok"], 0, "", ""));
+
+ let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default());
+
+ let mut auth: IndexMap<String, Option<String>> = IndexMap::new();
+ auth.insert("username".to_string(), Some("token".to_string()));
+ auth.insert("password".to_string(), Some(git_hub_token.to_string()));
+
+ let io = IOStub::new()
+ .with_is_interactive(false)
+ .with_has_authentication(true)
+ .with_get_authentication(auth);
+
+ let mut git = build_git(io, config, process);
+
+ git.__run_command(vec![command_callable], git_url, None, true, None)
+ .unwrap();
}
#[test]
-#[ignore = "requires getProcessExecutorMock and mocked IO (getMockBuilder IOInterface with hasAuthentication/getAuthentication/isInteractive expectations); no mocking infrastructure exists"]
-fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_with_authentication()
+fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_with_authentication_ssh()
{
- todo!()
+ run_command_private_github_with_authentication(
+ "git@github.com:acme/repo.git",
+ "ssh",
+ "MY_GITHUB_TOKEN",
+ "https://token:MY_GITHUB_TOKEN@github.com/acme/repo.git",
+ 1,
+ );
}
#[test]
-#[ignore = "requires getProcessExecutorMock and mocked IO/Config (getMockBuilder with hasAuthentication/getAuthentication/isInteractive expectations); no mocking infrastructure exists"]
-fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication()
+fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_with_authentication_https()
{
- todo!()
+ run_command_private_github_with_authentication(
+ "https://github.com/acme/repo",
+ "https",
+ "MY_GITHUB_TOKEN",
+ "https://token:MY_GITHUB_TOKEN@github.com/acme/repo.git",
+ 2,
+ );
+}
+
+// privateBitbucketWithCredentialsProvider helper.
+fn run_command_private_bitbucket_with_authentication(
+ git_url: &str,
+ bitbucket_token: Option<&str>,
+ expected_url: &str,
+ expected_failures_before_success: usize,
+ bitbucket_git_auth_calls: usize,
+) {
+ let expected_url_owned = expected_url.to_string();
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ if url != expected_url_owned {
+ return vec!["git command failing".to_string()];
+ }
+ vec!["git command ok".to_string()]
+ });
+
+ let config = ConfigStubBuilder::new()
+ .with(
+ "gitlab-domains",
+ PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]),
+ )
+ .with(
+ "github-domains",
+ PhpMixed::List(vec![PhpMixed::String("github.com".to_string())]),
+ )
+ .build();
+
+ let mut expected_calls: Vec<MockExpectation> = Vec::new();
+ for _ in 0..expected_failures_before_success {
+ expected_calls.push(cmd_full(vec!["git command failing"], 1, "", ""));
+ }
+ if bitbucket_git_auth_calls > 0 {
+ for _ in 0..bitbucket_git_auth_calls {
+ expected_calls.push(cmd_full(
+ vec!["git", "config", "bitbucket.accesstoken"],
+ 1,
+ "",
+ "",
+ ));
+ }
+ }
+ expected_calls.push(cmd_full(vec!["git command ok"], 0, "", ""));
+
+ let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default());
+
+ let mut io = IOStub::new().with_is_interactive(false);
+ if let Some(token) = bitbucket_token {
+ let mut auth: IndexMap<String, Option<String>> = IndexMap::new();
+ auth.insert("username".to_string(), Some("token".to_string()));
+ auth.insert("password".to_string(), Some(token.to_string()));
+ io = io
+ .with_has_authentication(true)
+ .with_get_authentication(auth);
+ }
+
+ let mut git = build_git(io, config, process);
+
+ git.__run_command(vec![command_callable], git_url, None, true, None)
+ .unwrap();
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_ssh_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "git@bitbucket.org:acme/repo.git",
+ Some("MY_BITBUCKET_TOKEN"),
+ "https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo",
+ Some("MY_BITBUCKET_TOKEN"),
+ "https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_git_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo.git",
+ Some("MY_BITBUCKET_TOKEN"),
+ "https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_ssh_no_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "git@bitbucket.org:acme/repo.git",
+ None,
+ "git@bitbucket.org:acme/repo.git",
+ 0,
+ 0,
+ );
}
#[test]
-#[ignore = "requires getProcessExecutorMock, getHttpDownloaderMock and mocked IO/Config with askConfirmation/askAndHideAnswer/setAuthentication expectations; no mocking infrastructure exists"]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_no_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo",
+ None,
+ "git@bitbucket.org:acme/repo.git",
+ 1,
+ 1,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_git_no_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo.git",
+ None,
+ "git@bitbucket.org:acme/repo.git",
+ 1,
+ 1,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_atat_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo.git",
+ Some("ATAT_BITBUCKET_API_TOKEN"),
+ "https://x-bitbucket-api-token-auth:ATAT_BITBUCKET_API_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+#[ignore = "interactive Bitbucket OAuth flow needs IOStub::askConfirmation/askAndHideAnswer/setAuthentication stateful callbacks and getHttpDownloaderMock; IOStub only supports fixed willReturn values, not the willReturnCallback-based stateful initial_config mutation this test relies on"]
fn test_run_command_private_bitbucket_repository_not_initial_clone_interactive_with_oauth() {
todo!()
}
#[test]
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and mocked Config/Filesystem (getMockBuilder removeDirectory); no mocking infrastructure exists"]
fn test_sync_mirror_sanitizes_url_after_initial_clone() {
- todo!()
+ let non_existent_dir = format!(
+ "{}/composer-test-nonexistent-{}",
+ std::env::temp_dir().display(),
+ rand_hex()
+ );
+
+ let config = mock_sync_mirror_config();
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd_full(
+ vec![
+ "git",
+ "clone",
+ "--mirror",
+ "--",
+ "https://user:secret@example.com/repo.git",
+ &non_existent_dir,
+ ],
+ 0,
+ "",
+ "",
+ ),
+ cmd_full(vec!["git", "remote", "-v"], 0, "", ""),
+ cmd_full(
+ vec![
+ "git",
+ "remote",
+ "set-url",
+ "origin",
+ "--",
+ "https://example.com/repo.git",
+ ],
+ 0,
+ "",
+ "",
+ ),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ let result = git
+ .sync_mirror(
+ "https://user:secret@example.com/repo.git",
+ &non_existent_dir,
+ )
+ .unwrap();
+
+ assert!(result);
}
#[test]
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and mocked Config; no mocking infrastructure exists"]
+#[ignore = "the failed-update branch reaches Git::throw_exception -> Url::sanitize, whose preg pattern fails to parse in the regex crate (preg shim bug, unrelated to this test); production Url::sanitize must be fixed first"]
fn test_sync_mirror_sanitizes_url_even_after_failed_update() {
- todo!()
+ let dir = std::env::temp_dir().display().to_string();
+
+ let config = mock_sync_mirror_config();
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd_full(vec!["git", "rev-parse", "--git-dir"], 0, ".\n", ""),
+ cmd_full(vec!["git", "remote", "-v"], 0, "", ""),
+ cmd_full(
+ vec![
+ "git",
+ "remote",
+ "set-url",
+ "origin",
+ "--",
+ "https://user:secret@example.com/repo.git",
+ ],
+ 0,
+ "",
+ "",
+ ),
+ cmd_full(
+ vec!["git", "remote", "update", "--prune", "origin"],
+ 1,
+ "",
+ "",
+ ),
+ cmd_full(vec!["git", "--version"], 0, "", ""),
+ cmd_full(vec!["git", "remote", "-v"], 0, "", ""),
+ cmd_full(
+ vec![
+ "git",
+ "remote",
+ "set-url",
+ "origin",
+ "--",
+ "https://example.com/repo.git",
+ ],
+ 0,
+ "",
+ "",
+ ),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ let result = git
+ .sync_mirror("https://user:secret@example.com/repo.git", &dir)
+ .unwrap();
+
+ assert!(!result);
+}
+
+fn rand_hex() -> String {
+ use std::time::{SystemTime, UNIX_EPOCH};
+ let nanos = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_nanos();
+ format!("{:016x}", nanos as u64)
}