diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 15:12:06 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-26 00:20:05 +0900 |
| commit | 8117e5450693d19726da877bce8aacf5c7aa53af (patch) | |
| tree | 4b06e6f6924599f3402d29cdcdeb0f8a88354d89 /crates/shirabe/tests/util | |
| parent | 8dd3d67884a204ca40e3364206868fea77a312be (diff) | |
| download | php-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')
| -rw-r--r-- | crates/shirabe/tests/util/git_test.rs | 478 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/url_test.rs | 1 |
2 files changed, 456 insertions, 23 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) } diff --git a/crates/shirabe/tests/util/url_test.rs b/crates/shirabe/tests/util/url_test.rs index 4adb4f2..3691d26 100644 --- a/crates/shirabe/tests/util/url_test.rs +++ b/crates/shirabe/tests/util/url_test.rs @@ -143,7 +143,6 @@ fn dist_refs_provider() -> Vec<( } #[test] -#[ignore] fn test_sanitize() { for (expected, url) in sanitize_provider() { assert_eq!(expected, Url::sanitize(url.to_string())); |
