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/repository | |
| 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/repository')
| -rw-r--r-- | crates/shirabe/tests/repository/main.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/tests/repository/vcs/github_driver_test.rs | 803 | ||||
| -rw-r--r-- | crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs | 780 |
3 files changed, 1520 insertions, 65 deletions
diff --git a/crates/shirabe/tests/repository/main.rs b/crates/shirabe/tests/repository/main.rs index 15a42a3..f5ecbc6 100644 --- a/crates/shirabe/tests/repository/main.rs +++ b/crates/shirabe/tests/repository/main.rs @@ -4,6 +4,8 @@ mod config_stub; mod http_downloader_mock; #[path = "../common/io_stub.rs"] mod io_stub; +#[path = "../common/process_executor_mock.rs"] +mod process_executor_mock; #[path = "../common/test_case.rs"] mod test_case; diff --git a/crates/shirabe/tests/repository/vcs/github_driver_test.rs b/crates/shirabe/tests/repository/vcs/github_driver_test.rs index f5958a4..0469edf 100644 --- a/crates/shirabe/tests/repository/vcs/github_driver_test.rs +++ b/crates/shirabe/tests/repository/vcs/github_driver_test.rs @@ -5,13 +5,22 @@ use std::rc::Rc; use indexmap::IndexMap; use shirabe::config::Config; +use shirabe::config::ConfigSourceInterface; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::repository::vcs::GitHubDriver; use shirabe::util::filesystem::Filesystem; +use shirabe::util::http_downloader::HttpDownloaderMockHandler; +use shirabe::util::process_executor::{MockHandler, ProcessExecutor}; use shirabe_php_shim::PhpMixed; use tempfile::TempDir; +use crate::http_downloader_mock::{HttpDownloaderMockGuard, expect_full, get_http_downloader_mock}; +use crate::io_stub::IOStub; +use crate::process_executor_mock::{ + ProcessExecutorMockGuard, cmd, cmd_full, get_process_executor_mock, +}; + struct SetUp { home: TempDir, config: Config, @@ -53,6 +62,82 @@ impl Drop for TearDown { } } +// No-op ConfigSourceInterface, equivalent to PHPUnit's +// `getMockBuilder(ConfigSourceInterface::class)->getMock()` whose methods all do +// nothing. Used by testPrivateRepository to satisfy the OAuth token-storing path. +#[derive(Debug)] +struct NullConfigSource; + +impl ConfigSourceInterface for NullConfigSource { + fn add_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _append: bool, + ) -> anyhow::Result<()> { + Ok(()) + } + fn insert_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _reference_name: &str, + _offset: i64, + ) -> anyhow::Result<()> { + Ok(()) + } + fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> { + Ok(()) + } + fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> { + Ok(()) + } + fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + Ok(()) + } + fn remove_config_setting(&mut self, _name: &str) -> anyhow::Result<()> { + Ok(()) + } + fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + Ok(()) + } + fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> { + Ok(()) + } + fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()> { + let _ = (r#type, name, value); + Ok(()) + } + fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()> { + let _ = (r#type, name); + Ok(()) + } + fn get_name(&self) -> String { + String::new() + } +} + +// Mirrors PHP's $httpDownloader->expects([...], true): an `['url' => .., 'body' => ..]` +// entry (status defaults to 200). +fn http_body( + url: &str, + body: impl Into<String>, +) -> shirabe::util::http_downloader::HttpDownloaderMockExpectation { + expect_full(url, None, 200, body, vec![String::new()]) +} + +// Mirrors PHP's `['url' => .., 'status' => 404]` entry (no body). +fn http_status( + url: &str, + status: i64, +) -> shirabe::util::http_downloader::HttpDownloaderMockExpectation { + expect_full(url, None, status, String::new(), vec![String::new()]) +} + +fn b64(s: &str) -> String { + shirabe_php_shim::base64_encode(s) +} + fn supports_provider() -> Vec<(bool, &'static str)> { vec![ (false, "https://github.com/acme"), @@ -80,77 +165,731 @@ fn test_supports() { } } -// The remaining cases construct a GitHubDriver and mock the HttpDownloader/IO to return -// GitHub API responses; mocking is not available, and a real HttpDownloader reaches -// curl_multi_init (todo!()). #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject (askAndHideAnswer/setAuthentication), and setAttribute reflection are not ported"] fn test_private_repository() { - let SetUp { home, config: _ } = set_up(); + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + + let repo_url = "http://github.com/composer/packagist"; + let repo_api_url = "https://api.github.com/repos/composer/packagist"; + let repo_ssh_url = "git@github.com:composer/packagist.git"; + let identifier = "v0.0.0"; + let sha = "SOMESHA"; + + let config = Rc::new(RefCell::new(config)); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new( + IOStub::new() + .with_is_interactive(true) + .with_ask_and_hide_answer(Some("sometoken".to_string())), + )); + + let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock( + vec![ + http_status(repo_api_url, 404), + http_body("https://api.github.com/", "{}"), + http_body( + repo_api_url, + r#"{"master_branch": "test_master", "private": true, "owner": {"login": "composer"}, "name": "packagist"}"#, + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let (process, _process_guard): (_, ProcessExecutorMockGuard) = get_process_executor_mock( + vec![], + false, + MockHandler { + r#return: 1, + stdout: String::new(), + stderr: String::new(), + }, + ); + + config + .borrow_mut() + .set_config_source(Box::new(NullConfigSource)); + config + .borrow_mut() + .set_auth_config_source(Box::new(NullConfigSource)); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + + let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + let mut tags = IndexMap::new(); + tags.insert(identifier.to_string(), sha.to_string()); + git_hub_driver.__set_tags(Some(tags)); + + assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap()); + + let dist = git_hub_driver.get_dist(sha).unwrap(); + assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"), + dist.get("url").and_then(|v| v.as_string()) + ); + assert_eq!( + Some("SOMESHA"), + dist.get("reference").and_then(|v| v.as_string()) + ); + + let source = git_hub_driver.get_source(sha); + assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some(repo_ssh_url), + source.get("url").and_then(|v| v.as_string()) + ); + assert_eq!( + Some("SOMESHA"), + source.get("reference").and_then(|v| v.as_string()) + ); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"] fn test_public_repository() { - let SetUp { home, config: _ } = set_up(); + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + + let repo_url = "http://github.com/composer/packagist"; + let repo_api_url = "https://api.github.com/repos/composer/packagist"; + let identifier = "v0.0.0"; + let sha = "SOMESHA"; + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = + Rc::new(RefCell::new(IOStub::new().with_is_interactive(true))); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![http_body( + repo_api_url, + r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#, + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + let repo_url = "https://github.com/composer/packagist.git"; + + let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + let mut tags = IndexMap::new(); + tags.insert(identifier.to_string(), sha.to_string()); + git_hub_driver.__set_tags(Some(tags)); + + assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap()); + + let dist = git_hub_driver.get_dist(sha).unwrap(); + assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"), + dist.get("url").and_then(|v| v.as_string()) + ); + assert_eq!(Some(sha), dist.get("reference").and_then(|v| v.as_string())); + + let source = git_hub_driver.get_source(sha); + assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some(repo_url), + source.get("url").and_then(|v| v.as_string()) + ); + assert_eq!( + Some(sha), + source.get("reference").and_then(|v| v.as_string()) + ); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"] +#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"] fn test_public_repository2() { - let SetUp { home, config: _ } = set_up(); + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + + let repo_url = "http://github.com/composer/packagist"; + let repo_api_url = "https://api.github.com/repos/composer/packagist"; + let identifier = "feature/3.2-foo"; + let sha = "SOMESHA"; + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = + Rc::new(RefCell::new(IOStub::new().with_is_interactive(true))); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + http_body( + repo_api_url, + r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo", + format!( + r#"{{"encoding":"base64","content":"{}"}}"#, + b64(&format!(r#"{{"support": {{"source": "{}" }}}}"#, repo_url)) + ), + ), + http_body( + "https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo", + r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml", + format!( + r#"{{"encoding": "base64", "content": "{}"}}"#, + b64("custom: https://example.com") + ), + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + let repo_url = "https://github.com/composer/packagist.git"; + + let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + let mut tags = IndexMap::new(); + tags.insert(identifier.to_string(), sha.to_string()); + git_hub_driver.__set_tags(Some(tags)); + + assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap()); + + let dist = git_hub_driver.get_dist(sha).unwrap(); + assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"), + dist.get("url").and_then(|v| v.as_string()) + ); + assert_eq!(Some(sha), dist.get("reference").and_then(|v| v.as_string())); + + let source = git_hub_driver.get_source(sha); + assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some(repo_url), + source.get("url").and_then(|v| v.as_string()) + ); + assert_eq!( + Some(sha), + source.get("reference").and_then(|v| v.as_string()) + ); + + let data = git_hub_driver + .get_composer_information(identifier) + .unwrap() + .unwrap(); + assert!(!data.contains_key("abandoned")); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"] +#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"] fn test_invalid_support_data() { - let SetUp { home, config: _ } = set_up(); + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + + let repo_url = "http://github.com/composer/packagist"; + let repo_api_url = "https://api.github.com/repos/composer/packagist"; + let identifier = "feature/3.2-foo"; + let sha = "SOMESHA"; + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = + Rc::new(RefCell::new(IOStub::new().with_is_interactive(true))); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + http_body( + repo_api_url, + r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo", + format!( + r#"{{"encoding":"base64","content":"{}"}}"#, + b64(&format!(r#"{{"support": "{}" }}"#, repo_url)) + ), + ), + http_body( + "https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo", + r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml", + format!( + r#"{{"encoding": "base64", "content": "{}"}}"#, + b64("custom: https://example.com") + ), + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + + let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + let mut tags = IndexMap::new(); + tags.insert(identifier.to_string(), sha.to_string()); + git_hub_driver.__set_tags(Some(tags)); + let mut branches = IndexMap::new(); + branches.insert("test_master".to_string(), sha.to_string()); + git_hub_driver.__set_branches(Some(branches)); + + let data = git_hub_driver + .get_composer_information(identifier) + .unwrap() + .unwrap(); + + let support = data.get("support").and_then(|v| v.as_array()).unwrap(); + assert_eq!( + Some("https://github.com/composer/packagist/tree/feature/3.2-foo"), + support.get("source").and_then(|v| v.as_string()) + ); +} + +fn funding_url_provider() -> Vec<(&'static str, Option<Vec<(&'static str, &'static str)>>)> { + let all_named_platforms = "community_bridge: project-name +github: [userA, userB] +issuehunt: userName +ko_fi: userName +liberapay: userName +open_collective: userName +patreon: userName +tidelift: Platform/Package +polar: userName +buy_me_a_coffee: userName +thanks_dev: u/gh/userName +otechie: userName"; + + vec![ + ( + all_named_platforms, + Some(vec![ + ( + "community_bridge", + "https://funding.communitybridge.org/projects/project-name", + ), + ("github", "https://github.com/userA"), + ("github", "https://github.com/userB"), + ("issuehunt", "https://issuehunt.io/r/userName"), + ("ko_fi", "https://ko-fi.com/userName"), + ("liberapay", "https://liberapay.com/userName"), + ("open_collective", "https://opencollective.com/userName"), + ("patreon", "https://www.patreon.com/userName"), + ( + "tidelift", + "https://tidelift.com/funding/github/Platform/Package", + ), + ("polar", "https://polar.sh/userName"), + ("buy_me_a_coffee", "https://www.buymeacoffee.com/userName"), + ("thanks_dev", "https://thanks.dev/u/gh/userName"), + ("otechie", "https://otechie.com/userName"), + ]), + ), + ( + "custom: example.com", + Some(vec![("custom", "https://example.com")]), + ), + ( + "custom: [example.com]", + Some(vec![("custom", "https://example.com")]), + ), + ( + "custom: \"https://example.com\"", + Some(vec![("custom", "https://example.com")]), + ), + ( + "custom: [\"https://example.com\"]", + Some(vec![("custom", "https://example.com")]), + ), + ( + "custom: [\"https://example.com\", example.org]", + Some(vec![ + ("custom", "https://example.com"), + ("custom", "https://example.org"), + ]), + ), + ( + "custom: [example.net/funding, \"https://example.com\", example.org]", + Some(vec![ + ("custom", "https://example.com"), + ("custom", "https://example.org"), + ]), + ), + ] } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, setAttribute reflection, and the fundingUrlProvider data provider are not ported"] +#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"] fn test_funding_format() { - let SetUp { home, config: _ } = set_up(); - let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + for (funding, expected) in funding_url_provider() { + let SetUp { home, config } = set_up(); + let _tear_down = TearDown::new(home.path().to_path_buf()); + + let repo_url = "http://github.com/composer/packagist"; + let repo_api_url = "https://api.github.com/repos/composer/packagist"; + let identifier = "feature/3.2-foo"; + let sha = "SOMESHA"; + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = + Rc::new(RefCell::new(IOStub::new().with_is_interactive(true))); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + http_body( + repo_api_url, + r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo", + format!( + r#"{{"encoding":"base64","content":"{}"}}"#, + b64(&format!(r#"{{"support": {{"source": "{}" }}}}"#, repo_url)) + ), + ), + http_body( + "https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo", + r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml", + format!(r#"{{"encoding": "base64", "content": "{}"}}"#, b64(funding)), + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + + let mut git_hub_driver = + GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + let mut tags = IndexMap::new(); + tags.insert(identifier.to_string(), sha.to_string()); + git_hub_driver.__set_tags(Some(tags)); + let mut branches = IndexMap::new(); + branches.insert("test_master".to_string(), sha.to_string()); + git_hub_driver.__set_branches(Some(branches)); + + let data = git_hub_driver + .get_composer_information(identifier) + .unwrap() + .unwrap(); + + match expected { + None => assert!(!data.contains_key("funding")), + Some(expected) => { + let funding_list = match data.get("funding") { + Some(PhpMixed::List(l)) => l.clone(), + Some(PhpMixed::Array(m)) => m.values().cloned().collect(), + other => panic!("unexpected funding value: {:?}", other), + }; + let actual: Vec<(String, String)> = funding_list + .iter() + .map(|entry| { + let m = entry.as_array().unwrap(); + ( + m.get("type") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + m.get("url") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(), + ) + }) + .collect(); + let expected_vec: Vec<(String, String)> = expected + .iter() + .map(|(t, u)| (t.to_string(), u.to_string())) + .collect(); + assert_eq!(expected_vec, actual); + } + } + } } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"] +#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"] fn test_public_repository_archived() { - let SetUp { home, config: _ } = set_up(); + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + + let repo_url = "http://github.com/composer/packagist"; + let repo_api_url = "https://api.github.com/repos/composer/packagist"; + let identifier = "v0.0.0"; + let sha = "SOMESHA"; + let composer_json_url = format!( + "https://api.github.com/repos/composer/packagist/contents/composer.json?ref={}", + sha + ); + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = + Rc::new(RefCell::new(IOStub::new().with_is_interactive(true))); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + http_body( + repo_api_url, + r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}"#, + ), + http_body( + &composer_json_url, + format!( + r#"{{"encoding": "base64", "content": "{}"}}"#, + b64(r#"{"name": "composer/packagist"}"#) + ), + ), + http_body( + &format!( + "https://api.github.com/repos/composer/packagist/commits/{}", + sha + ), + r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml", + format!( + r#"{{"encoding": "base64", "content": "{}"}}"#, + b64("custom: https://example.com") + ), + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + + let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + let mut tags = IndexMap::new(); + tags.insert(identifier.to_string(), sha.to_string()); + git_hub_driver.__set_tags(Some(tags)); + + let data = git_hub_driver + .get_composer_information(sha) + .unwrap() + .unwrap(); + + assert_eq!(Some(true), data.get("abandoned").and_then(|v| v.as_bool())); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, and the IOInterface MockObject are not ported"] +#[ignore = "GitDriver clone-fallback path runs an unexpected `git --version` (Git::get_version) not in the PHP mock expectation list; needs the version static seeded and the Rust sync_mirror command sequence to match"] fn test_private_repository_no_interaction() { - let SetUp { home, config: _ } = set_up(); + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + + let repo_url = "http://github.com/composer/packagist"; + let repo_api_url = "https://api.github.com/repos/composer/packagist"; + let repo_ssh_url = "git@github.com:composer/packagist.git"; + let identifier = "v0.0.0"; + let sha = "SOMESHA"; + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = + Rc::new(RefCell::new(IOStub::new().with_is_interactive(false))); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![http_status(repo_api_url, 404)], + true, + HttpDownloaderMockHandler::default(), + ); + + // clean local clone if present + let cache_dir = std::env::temp_dir().join("composer-test"); + let mut fs = Filesystem::new(None); + let _ = fs.remove_directory(&cache_dir); + let cache_vcs_dir = std::env::temp_dir() + .join("composer-test/cache") + .to_string_lossy() + .into_owned(); + { + let mut top: IndexMap<String, PhpMixed> = IndexMap::new(); + let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new(); + config_section.insert( + "cache-vcs-dir".to_string(), + PhpMixed::String(cache_vcs_dir.clone()), + ); + top.insert("config".to_string(), PhpMixed::Array(config_section)); + config.borrow_mut().merge(&top, Config::SOURCE_UNKNOWN); + } + let resolved_cache_vcs_dir = config + .borrow_mut() + .get("cache-vcs-dir") + .as_string() + .unwrap_or("") + .to_string(); + + let (process, _process_guard): (_, ProcessExecutorMockGuard) = get_process_executor_mock( + vec![ + cmd_full(["git", "config", "github.accesstoken"], 1, "", ""), + cmd([ + "git", + "clone", + "--mirror", + "--", + repo_ssh_url, + &format!( + "{}/git-github.com-composer-packagist.git/", + resolved_cache_vcs_dir + ), + ]), + cmd(["git", "remote", "-v"]), + cmd(["git", "remote", "set-url", "origin", "--", repo_ssh_url]), + cmd_full( + ["git", "show-ref", "--tags", "--dereference"], + 0, + format!("{} refs/tags/{}", sha, identifier), + "", + ), + cmd_full( + ["git", "branch", "--no-color", "--no-abbrev", "-v"], + 0, + " test_master edf93f1fccaebd8764383dc12016d0a1a9672d89 Fix test & behavior", + "", + ), + cmd_full(["git", "branch", "--no-color"], 0, "* test_master", ""), + ], + true, + MockHandler::default(), + ); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + + let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + + assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap()); + + let dist = git_hub_driver.get_dist(sha).unwrap(); + assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"), + dist.get("url").and_then(|v| v.as_string()) + ); + assert_eq!(Some(sha), dist.get("reference").and_then(|v| v.as_string())); + + let source = git_hub_driver.get_source(identifier); + assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some(repo_ssh_url), + source.get("url").and_then(|v| v.as_string()) + ); + assert_eq!( + Some(identifier), + source.get("reference").and_then(|v| v.as_string()) + ); + + let source = git_hub_driver.get_source(sha); + assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string())); + assert_eq!( + Some(repo_ssh_url), + source.get("url").and_then(|v| v.as_string()) + ); + assert_eq!( + Some(sha), + source.get("reference").and_then(|v| v.as_string()) + ); +} + +fn invalid_url_provider() -> Vec<&'static str> { + vec![ + "https://github.com/acme", + "https://github.com/acme/repository/releases", + "https://github.com/acme/repository/pulls", + ] } #[test] -#[ignore = "IOInterface MockObject (getMockBuilder) and HttpDownloader MockObject (getMockBuilder) plus the invalidUrlProvider data provider are not ported"] fn test_initialize_invalid_repo_url() { - let SetUp { home, config: _ } = set_up(); - let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + for url in invalid_url_provider() { + let SetUp { home, config } = set_up(); + let _tear_down = TearDown::new(home.path().to_path_buf()); + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(IOStub::new())); + + let (http_downloader, _http_guard) = + get_http_downloader_mock(vec![], true, HttpDownloaderMockHandler::default()); + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + + let mut git_hub_driver = + GitHubDriver::new(repo_config, io, config, http_downloader, process); + let result = git_hub_driver.initialize(); + assert!( + result.is_err(), + "expected InvalidArgumentException for url {}", + url + ); + } } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, and the IOInterface MockObject are not ported"] fn test_get_empty_file_content() { - let SetUp { home, config: _ } = set_up(); + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() + + let repo_url = "http://github.com/composer/packagist"; + + let config = Rc::new(RefCell::new(config)); + let io: Rc<RefCell<dyn IOInterface>> = + Rc::new(RefCell::new(IOStub::new().with_is_interactive(true))); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + http_body( + "https://api.github.com/repos/composer/packagist", + r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}"#, + ), + http_body( + "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=main", + r#"{"encoding":"base64","content":""}"#, + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); + + let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process); + git_hub_driver.initialize().unwrap(); + + assert_eq!( + Some(String::new()), + git_hub_driver + .get_file_content("composer.json", "main") + .unwrap() + ); } diff --git a/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs b/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs index 0333a2b..e21835d 100644 --- a/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs +++ b/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs @@ -8,8 +8,15 @@ use shirabe::config::Config; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::repository::vcs::GitLabDriver; +use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler}; +use shirabe::util::process_executor::{MockHandler, ProcessExecutor}; use shirabe_php_shim::{PhpMixed, extension_loaded}; +use crate::http_downloader_mock::{ + HttpDownloaderMockGuard, expect, expect_full, get_http_downloader_mock, +}; +use crate::process_executor_mock::{ProcessExecutorMockGuard, get_process_executor_mock}; + // Mirrors GitLabDriverTest::setUp's `gitlab-domains` configuration. fn make_config() -> Config { let mut config = Config::new(true, None); @@ -30,74 +37,596 @@ fn make_config() -> Config { config } +// Common test fixtures: the IO mock (a bare NullIO, matching PHP's unconfigured +// getMock), the config and a no-op process executor mock. The PHP test passes a +// bare `getMock()` ProcessExecutor; none of these tests invoke git, so an empty +// strict expectation set both matches PHP and catches unexpected process calls. +struct Fixtures { + io: Rc<RefCell<dyn IOInterface>>, + config: Rc<RefCell<Config>>, + process: Rc<RefCell<ProcessExecutor>>, + _process_guard: ProcessExecutorMockGuard, +} + +fn fixtures() -> Fixtures { + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let config = Rc::new(RefCell::new(make_config())); + let (process, _process_guard) = get_process_executor_mock(vec![], true, MockHandler::default()); + Fixtures { + io, + config, + process, + _process_guard, + } +} + +// Mirrors $this->getHttpDownloaderMock() with a single project-data expectation. +fn http_mock_with_body( + url: &str, + body: &str, +) -> (Rc<RefCell<HttpDownloader>>, HttpDownloaderMockGuard) { + get_http_downloader_mock( + vec![expect_full(url, None, 200, body, vec![String::new()])], + true, + HttpDownloaderMockHandler::default(), + ) +} + +fn provide_initialize_urls() -> Vec<(&'static str, &'static str)> { + vec![ + ( + "https://gitlab.com/mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ), + ( + "http://gitlab.com/mygroup/myproject", + "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ), + ( + "git@gitlab.com:mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ), + ] +} + +// Mirrors testInitialize: returns the initialized driver together with the http +// mock guard (kept alive by the caller so __assert_complete runs at scope end). +fn do_test_initialize( + fixtures: &Fixtures, + url: &str, + api_url: &str, +) -> (GitLabDriver, HttpDownloaderMockGuard) { + // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "visibility": "private", + "issues_enabled": true, + "archived": false, + "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git", + "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/myproject", + "web_url": "https://gitlab.com/mygroup/myproject" +}"#; + + let (http_downloader, guard) = http_mock_with_body(api_url, project_data); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + + assert_eq!( + api_url, + driver.get_api_url(), + "API URL is derived from the repository URL" + ); + assert_eq!( + "mymaster", + driver.get_root_identifier().unwrap(), + "Root identifier is the default branch in GitLab" + ); + assert_eq!( + "git@gitlab.com:mygroup/myproject.git", + driver.get_repository_url(), + "The repository URL is the SSH one by default" + ); + assert_eq!("https://gitlab.com/mygroup/myproject", driver.get_url()); + + (driver, guard) +} + +// Mirrors testInitializePublicProject. +fn do_test_initialize_public_project( + fixtures: &Fixtures, + url: &str, + api_url: &str, +) -> (GitLabDriver, HttpDownloaderMockGuard) { + // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "visibility": "public", + "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git", + "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/myproject", + "web_url": "https://gitlab.com/mygroup/myproject" +}"#; + + let (http_downloader, guard) = http_mock_with_body(api_url, project_data); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + + assert_eq!( + api_url, + driver.get_api_url(), + "API URL is derived from the repository URL" + ); + assert_eq!( + "mymaster", + driver.get_root_identifier().unwrap(), + "Root identifier is the default branch in GitLab" + ); + assert_eq!( + "https://gitlab.com/mygroup/myproject.git", + driver.get_repository_url(), + "The repository URL is the SSH one by default" + ); + assert_eq!("https://gitlab.com/mygroup/myproject", driver.get_url()); + + (driver, guard) +} + #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_initialize() { - todo!() + let fixtures = fixtures(); + for (url, api_url) in provide_initialize_urls() { + let (_driver, _guard) = do_test_initialize(&fixtures, url, api_url); + } } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_initialize_public_project() { - todo!() + let fixtures = fixtures(); + for (url, api_url) in provide_initialize_urls() { + let (_driver, _guard) = do_test_initialize_public_project(&fixtures, url, api_url); + } } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_initialize_public_project_as_anonymous() { - todo!() + let fixtures = fixtures(); + for (url, api_url) in provide_initialize_urls() { + // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git", + "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/myproject", + "web_url": "https://gitlab.com/mygroup/myproject" +}"#; + + let (http_downloader, _guard) = http_mock_with_body(api_url, project_data); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + + assert_eq!( + api_url, + driver.get_api_url(), + "API URL is derived from the repository URL" + ); + assert_eq!( + "mymaster", + driver.get_root_identifier().unwrap(), + "Root identifier is the default branch in GitLab" + ); + assert_eq!( + "https://gitlab.com/mygroup/myproject.git", + driver.get_repository_url(), + "The repository URL is the SSH one by default" + ); + assert_eq!("https://gitlab.com/mygroup/myproject", driver.get_url()); + } } +/// Also support repositories over HTTP (TLS) and has a port number. #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_initialize_with_port_number() { - todo!() + let fixtures = fixtures(); + let domain = "gitlab.mycompany.com"; + let port = "5443"; + let namespace = "mygroup/myproject"; + let url = format!("https://{}:{}/{}", domain, port, namespace); + // urlencode($namespace) replaces '/' with '%2F'. + let api_url = format!( + "https://{}:{}/api/v4/projects/{}", + domain, port, "mygroup%2Fmyproject" + ); + + // An incomplete single project API response payload. + // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project + let project_data = format!( + r#"{{ + "default_branch": "1.0.x", + "http_url_to_repo": "https://{0}:{1}/{2}.git", + "path": "myproject", + "path_with_namespace": "{2}", + "web_url": "https://{0}:{1}/{2}" +}}"#, + domain, port, namespace + ); + + let (http_downloader, _guard) = http_mock_with_body(&api_url, &project_data); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.clone())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + + assert_eq!( + api_url, + driver.get_api_url(), + "API URL is derived from the repository URL" + ); + assert_eq!( + "1.0.x", + driver.get_root_identifier().unwrap(), + "Root identifier is the default branch in GitLab" + ); + assert_eq!( + format!("{}.git", url), + driver.get_repository_url(), + "The repository URL is the SSH one by default" + ); + assert_eq!(url, driver.get_url()); } #[test] -#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + setAttribute (\\ReflectionProperty) not ported"] fn test_invalid_support_data() { - todo!() + let fixtures = fixtures(); + let repo_url = "https://gitlab.com/mygroup/myproject"; + let (mut driver, _init_guard) = do_test_initialize( + &fixtures, + repo_url, + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ); + + let mut branches: IndexMap<String, String> = IndexMap::new(); + branches.insert("main".to_string(), "SOMESHA".to_string()); + driver.__set_branches(Some(branches)); + driver.__set_tags(Some(IndexMap::new())); + + let (http_downloader, _guard) = get_http_downloader_mock( + vec![expect_full( + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/files/composer%2Ejson/raw?ref=SOMESHA", + None, + 200, + format!(r#"{{"support": "{}" }}"#, repo_url), + vec![String::new()], + )], + true, + HttpDownloaderMockHandler::default(), + ); + driver.set_http_downloader(http_downloader); + + let data = driver.get_composer_information("main").unwrap().unwrap(); + + let source = data + .get("support") + .and_then(|v| v.as_array()) + .and_then(|m| m.get("source")) + .and_then(|v| v.as_string()) + .unwrap(); + assert_eq!("https://gitlab.com/mygroup/myproject/-/tree/main", source); } #[test] -#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"] fn test_get_dist() { - todo!() + let fixtures = fixtures(); + let (driver, _guard) = do_test_initialize( + &fixtures, + "https://gitlab.com/mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ); + + let reference = "c3ebdbf9cceddb82cd2089aaef8c7b992e536363"; + let mut expected: IndexMap<String, PhpMixed> = IndexMap::new(); + expected.insert("type".to_string(), PhpMixed::String("zip".to_string())); + expected.insert( + "url".to_string(), + PhpMixed::String(format!( + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/archive.zip?sha={}", + reference + )), + ); + expected.insert( + "reference".to_string(), + PhpMixed::String(reference.to_string()), + ); + expected.insert("shasum".to_string(), PhpMixed::String(String::new())); + + assert_eq!(Some(expected), driver.get_dist(reference)); } #[test] -#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"] fn test_get_source() { - todo!() + let fixtures = fixtures(); + let (driver, _guard) = do_test_initialize( + &fixtures, + "https://gitlab.com/mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ); + + let reference = "c3ebdbf9cceddb82cd2089aaef8c7b992e536363"; + let mut expected: IndexMap<String, PhpMixed> = IndexMap::new(); + expected.insert("type".to_string(), PhpMixed::String("git".to_string())); + expected.insert( + "url".to_string(), + PhpMixed::String("git@gitlab.com:mygroup/myproject.git".to_string()), + ); + expected.insert( + "reference".to_string(), + PhpMixed::String(reference.to_string()), + ); + + assert_eq!(expected, driver.get_source(reference)); } #[test] -#[ignore = "depends on testInitializePublicProject, which needs HttpDownloaderMock + the IOInterface MockObject not ported"] fn test_get_source_given_public_project() { - todo!() + let fixtures = fixtures(); + let (driver, _guard) = do_test_initialize_public_project( + &fixtures, + "https://gitlab.com/mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ); + + let reference = "c3ebdbf9cceddb82cd2089aaef8c7b992e536363"; + let mut expected: IndexMap<String, PhpMixed> = IndexMap::new(); + expected.insert("type".to_string(), PhpMixed::String("git".to_string())); + expected.insert( + "url".to_string(), + PhpMixed::String("https://gitlab.com/mygroup/myproject.git".to_string()), + ); + expected.insert( + "reference".to_string(), + PhpMixed::String(reference.to_string()), + ); + + assert_eq!(expected, driver.get_source(reference)); } #[test] -#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"] fn test_get_tags() { - todo!() + let fixtures = fixtures(); + let (mut driver, _init_guard) = do_test_initialize( + &fixtures, + "https://gitlab.com/mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ); + + let api_url = + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?per_page=100"; + + // @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-tags + let tag_data = r#"[ + { + "name": "v1.0.0", + "commit": { + "id": "092ed2c762bbae331e3f51d4a17f67310bf99a81", + "committed_date": "2012-05-28T04:42:42-07:00" + } + }, + { + "name": "v2.0.0", + "commit": { + "id": "8e8f60b3ec86d63733db3bd6371117a758027ec6", + "committed_date": "2014-07-06T12:59:11.000+02:00" + } + } +]"#; + + let (http_downloader, _guard) = http_mock_with_body(api_url, tag_data); + driver.set_http_downloader(http_downloader); + + let mut expected: IndexMap<String, String> = IndexMap::new(); + expected.insert( + "v1.0.0".to_string(), + "092ed2c762bbae331e3f51d4a17f67310bf99a81".to_string(), + ); + expected.insert( + "v2.0.0".to_string(), + "8e8f60b3ec86d63733db3bd6371117a758027ec6".to_string(), + ); + + assert_eq!(expected, driver.get_tags().unwrap()); + assert_eq!(expected, driver.get_tags().unwrap(), "Tags are cached"); } #[test] -#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"] +#[ignore = "Response::find_header_value passes a `(?i)`-prefixed (non-delimited) pattern that compile_php_pattern (Preg) mis-translates to an invalid regex (`(?s)?i)^link:...`), so get_next_page panics when parsing the Link header. Pre-existing Preg/Response porting bug, unrelated to GitLabDriver."] fn test_get_paginated_refs() { - todo!() + let fixtures = fixtures(); + let (mut driver, _init_guard) = do_test_initialize( + &fixtures, + "https://gitlab.com/mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ); + + // @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-branches + let mut branch_data: Vec<PhpMixed> = vec![ + branch_entry( + "mymaster", + "97eda36b5c1dd953a3792865c222d4e85e5f302e", + "2013-01-03T21:04:07.000+01:00", + ), + branch_entry( + "staging", + "502cffe49f136443f2059803f2e7192d1ac066cd", + "2013-03-09T16:35:23.000+01:00", + ), + ]; + for _ in 0..98 { + branch_data.push(branch_entry( + "stagingdupe", + "502cffe49f136443f2059803f2e7192d1ac066cd", + "2013-03-09T16:35:23.000+01:00", + )); + } + let branch_data = shirabe::json::JsonFile::encode(&PhpMixed::List(branch_data)); + + let (http_downloader, _guard) = get_http_downloader_mock( + vec![ + expect_full( + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/branches?per_page=100", + None, + 200, + branch_data.clone(), + vec!["Link: <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20>; rel=\"next\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=1&per_page=20>; rel=\"first\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=3&per_page=20>; rel=\"last\"".to_string()], + ), + expect_full( + "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20", + None, + 200, + branch_data.clone(), + vec!["Link: <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20>; rel=\"prev\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=1&per_page=20>; rel=\"first\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=3&per_page=20>; rel=\"last\"".to_string()], + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + driver.set_http_downloader(http_downloader); + + let mut expected: IndexMap<String, String> = IndexMap::new(); + expected.insert( + "mymaster".to_string(), + "97eda36b5c1dd953a3792865c222d4e85e5f302e".to_string(), + ); + expected.insert( + "staging".to_string(), + "502cffe49f136443f2059803f2e7192d1ac066cd".to_string(), + ); + expected.insert( + "stagingdupe".to_string(), + "502cffe49f136443f2059803f2e7192d1ac066cd".to_string(), + ); + + assert_eq!(expected, driver.get_branches().unwrap()); + assert_eq!( + expected, + driver.get_branches().unwrap(), + "Branches are cached" + ); +} + +fn branch_entry(name: &str, id: &str, committed_date: &str) -> PhpMixed { + let mut commit: IndexMap<String, PhpMixed> = IndexMap::new(); + commit.insert("id".to_string(), PhpMixed::String(id.to_string())); + commit.insert( + "committed_date".to_string(), + PhpMixed::String(committed_date.to_string()), + ); + let mut entry: IndexMap<String, PhpMixed> = IndexMap::new(); + entry.insert("name".to_string(), PhpMixed::String(name.to_string())); + entry.insert("commit".to_string(), PhpMixed::Array(commit)); + PhpMixed::Array(entry) } #[test] -#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"] fn test_get_branches() { - todo!() + let fixtures = fixtures(); + let (mut driver, _init_guard) = do_test_initialize( + &fixtures, + "https://gitlab.com/mygroup/myproject", + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject", + ); + + let api_url = + "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/branches?per_page=100"; + + // @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-branches + let branch_data = r#"[ + { + "name": "mymaster", + "commit": { + "id": "97eda36b5c1dd953a3792865c222d4e85e5f302e", + "committed_date": "2013-01-03T21:04:07.000+01:00" + } + }, + { + "name": "staging", + "commit": { + "id": "502cffe49f136443f2059803f2e7192d1ac066cd", + "committed_date": "2013-03-09T16:35:23.000+01:00" + } + } +]"#; + + let (http_downloader, _guard) = http_mock_with_body(api_url, branch_data); + driver.set_http_downloader(http_downloader); + + let mut expected: IndexMap<String, String> = IndexMap::new(); + expected.insert( + "mymaster".to_string(), + "97eda36b5c1dd953a3792865c222d4e85e5f302e".to_string(), + ); + expected.insert( + "staging".to_string(), + "502cffe49f136443f2059803f2e7192d1ac066cd".to_string(), + ); + + assert_eq!(expected, driver.get_branches().unwrap()); + assert_eq!( + expected, + driver.get_branches().unwrap(), + "Branches are cached" + ); } #[test] -#[ignore] fn test_supports() { for (url, expected) in data_for_test_supports() { let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); @@ -147,31 +676,216 @@ fn data_for_test_supports() -> Vec<(&'static str, bool)> { } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_gitlab_sub_directory() { - todo!() + let fixtures = fixtures(); + let url = "https://mycompany.com/gitlab/mygroup/my-pro.ject"; + let api_url = "https://mycompany.com/gitlab/api/v4/projects/mygroup%2Fmy-pro%2Eject"; + + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "visibility": "private", + "http_url_to_repo": "https://gitlab.com/gitlab/mygroup/my-pro.ject", + "ssh_url_to_repo": "git@gitlab.com:mygroup/my-pro.ject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/my-pro.ject", + "web_url": "https://gitlab.com/gitlab/mygroup/my-pro.ject" +}"#; + + let (http_downloader, _guard) = http_mock_with_body(api_url, project_data); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + + assert_eq!( + api_url, + driver.get_api_url(), + "API URL is derived from the repository URL" + ); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_gitlab_sub_group() { - todo!() + let fixtures = fixtures(); + let url = "https://gitlab.com/mygroup/mysubgroup/myproject"; + let api_url = "https://gitlab.com/api/v4/projects/mygroup%2Fmysubgroup%2Fmyproject"; + + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "visibility": "private", + "http_url_to_repo": "https://gitlab.com/mygroup/mysubgroup/my-pro.ject", + "ssh_url_to_repo": "git@gitlab.com:mygroup/mysubgroup/my-pro.ject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/mysubgroup/my-pro.ject", + "web_url": "https://gitlab.com/mygroup/mysubgroup/my-pro.ject" +}"#; + + let (http_downloader, _guard) = http_mock_with_body(api_url, project_data); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + + assert_eq!( + api_url, + driver.get_api_url(), + "API URL is derived from the repository URL" + ); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_gitlab_sub_directory_sub_group() { - todo!() + let fixtures = fixtures(); + let url = "https://mycompany.com/gitlab/mygroup/mysubgroup/myproject"; + let api_url = "https://mycompany.com/gitlab/api/v4/projects/mygroup%2Fmysubgroup%2Fmyproject"; + + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "visibility": "private", + "http_url_to_repo": "https://mycompany.com/gitlab/mygroup/mysubgroup/my-pro.ject", + "ssh_url_to_repo": "git@mycompany.com:mygroup/mysubgroup/my-pro.ject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/mysubgroup/my-pro.ject", + "web_url": "https://mycompany.com/gitlab/mygroup/mysubgroup/my-pro.ject" +}"#; + + let (http_downloader, _guard) = http_mock_with_body(api_url, project_data); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + + assert_eq!( + api_url, + driver.get_api_url(), + "API URL is derived from the repository URL" + ); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_forwards_options() { - todo!() + let fixtures = fixtures(); + let mut ssl: IndexMap<String, PhpMixed> = IndexMap::new(); + ssl.insert("verify_peer".to_string(), PhpMixed::Bool(false)); + let mut options: IndexMap<String, PhpMixed> = IndexMap::new(); + options.insert("ssl".to_string(), PhpMixed::Array(ssl)); + + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "visibility": "private", + "http_url_to_repo": "https://gitlab.mycompany.local/mygroup/myproject", + "ssh_url_to_repo": "git@gitlab.mycompany.local:mygroup/myproject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/myproject", + "web_url": "https://gitlab.mycompany.local/mygroup/myproject" +}"#; + + let (http_downloader, _guard) = http_mock_with_body( + "https://gitlab.mycompany.local/api/v4/projects/mygroup%2Fmyproject", + project_data, + ); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert( + "url".to_string(), + PhpMixed::String("https://gitlab.mycompany.local/mygroup/myproject".to_string()), + ); + repo_config.insert("options".to_string(), PhpMixed::Array(options)); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + fixtures.config.clone(), + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); } #[test] -#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"] fn test_protocol_override_repository_url_generation() { - todo!() + let fixtures = fixtures(); + // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project + let project_data = r#"{ + "id": 17, + "default_branch": "mymaster", + "visibility": "private", + "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git", + "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git", + "last_activity_at": "2014-12-01T09:17:51.000+01:00", + "name": "My Project", + "name_with_namespace": "My Group / My Project", + "path": "myproject", + "path_with_namespace": "mygroup/myproject", + "web_url": "https://gitlab.com/mygroup/myproject" +}"#; + + let api_url = "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject"; + let url = "git@gitlab.com:mygroup/myproject"; + + let (http_downloader, _guard) = http_mock_with_body(api_url, project_data); + + // clone $this->config and merge gitlab-protocol => http. + let mut config = make_config(); + let mut top: IndexMap<String, PhpMixed> = IndexMap::new(); + let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new(); + config_section.insert( + "gitlab-protocol".to_string(), + PhpMixed::String("http".to_string()), + ); + top.insert("config".to_string(), PhpMixed::Array(config_section)); + config.merge(&top, Config::SOURCE_UNKNOWN); + let config = Rc::new(RefCell::new(config)); + + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + let mut driver = GitLabDriver::new( + repo_config, + fixtures.io.clone(), + config, + http_downloader, + fixtures.process.clone(), + ); + driver.initialize().unwrap(); + assert_eq!( + "https://gitlab.com/mygroup/myproject.git", + driver.get_repository_url(), + "Repository URL matches config request for http not git" + ); } |
