diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 17:02:11 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-26 00:20:05 +0900 |
| commit | 3498bb1ca00ab7d051d296b8d482bea987a00fa4 (patch) | |
| tree | e10260e5816317f4547847e2a230ea1a87fb2448 /crates/shirabe/tests/util | |
| parent | 291b43d132749a61918dca23acef1b639c5333a7 (diff) | |
| download | php-shirabe-3498bb1ca00ab7d051d296b8d482bea987a00fa4.tar.gz php-shirabe-3498bb1ca00ab7d051d296b8d482bea987a00fa4.tar.zst php-shirabe-3498bb1ca00ab7d051d296b8d482bea987a00fa4.zip | |
test: port 24 command/repository/package/util tests; add TlsHelper
Port command (9), util gitlab/forgejo/tls (6), package (6), repository (3)
tests. Implement TlsHelper. Fix porting bugs: config_command extra merge,
RootAliasPackage setters, ValidatingArrayLoader isset, repository_factory name
generation, forgejo exception code, version_parser error chaining.
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/forgejo_test.rs | 183 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/gitlab_test.rs | 171 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/tls_helper_test.rs | 133 |
3 files changed, 469 insertions, 18 deletions
diff --git a/crates/shirabe/tests/util/forgejo_test.rs b/crates/shirabe/tests/util/forgejo_test.rs index e8bf766..fd6dbc9 100644 --- a/crates/shirabe/tests/util/forgejo_test.rs +++ b/crates/shirabe/tests/util/forgejo_test.rs @@ -1,17 +1,186 @@ //! ref: composer/tests/Composer/Test/Util/ForgejoTest.php -// Both cases construct Forgejo with a mocked IO/Config/JsonConfigSource and a mocked -// HttpDownloader to drive the username/password authentication flow. Mocking is not -// available, and a real HttpDownloader reaches curl_multi_init (todo!()). +use std::cell::RefCell; +use std::rc::Rc; + +use shirabe::config::{Config, ConfigSourceInterface}; +use shirabe::io::IOInterface; +use shirabe::io::io_interface; +use shirabe::util::Forgejo; +use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler}; +use shirabe_php_shim::PhpMixed; + +use crate::config_stub::ConfigStubBuilder; +use crate::http_downloader_mock::{expect_full, get_http_downloader_mock}; +use crate::io_mock::{Expectation, IOMock, get_io_mock}; + +const USERNAME: &str = "username"; +const ACCESS_TOKEN: &str = "access-token"; +const MESSAGE: &str = "mymessage"; +const ORIGIN: &str = "codeberg.org"; + +// Records the config setting names a source has had removed, plus a fixed getName, +// mirroring ForgejoTest's JsonConfigSource mocks (getName -> "auth.json", and the +// config source stubbing removeConfigSetting('forgejo-token.<origin>')). +#[derive(Debug)] +struct ConfigSourceMock { + name: String, + removed: Rc<RefCell<Vec<String>>>, +} + +impl ConfigSourceMock { + fn new(name: &str) -> (Box<Self>, Rc<RefCell<Vec<String>>>) { + let removed = Rc::new(RefCell::new(Vec::new())); + ( + Box::new(Self { + name: name.to_string(), + removed: removed.clone(), + }), + removed, + ) + } +} + +impl ConfigSourceInterface for ConfigSourceMock { + fn add_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _append: bool, + ) -> anyhow::Result<()> { + unreachable!() + } + fn insert_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _reference_name: &str, + _offset: i64, + ) -> anyhow::Result<()> { + unreachable!() + } + fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + Ok(()) + } + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()> { + self.removed.borrow_mut().push(name.to_string()); + Ok(()) + } + fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + unreachable!() + } + fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn add_link(&mut self, _type: &str, _name: &str, _value: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_link(&mut self, _type: &str, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn get_name(&self) -> String { + self.name.clone() + } +} + +fn build_forgejo( + io_mock: &Rc<RefCell<IOMock>>, + config: Rc<RefCell<Config>>, + http_downloader: Rc<RefCell<HttpDownloader>>, +) -> Forgejo { + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + Forgejo::new(io, config, http_downloader) +} #[test] -#[ignore = "requires getIOMock/getHttpDownloaderMock and getMockBuilder mocks of Config/JsonConfigSource with expects()/willReturn(); no mocking infrastructure exists"] fn test_username_password_authentication_flow() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::text(MESSAGE), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Token (hidden): ", ACCESS_TOKEN), + ], + false, + ) + .unwrap(); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![expect_full( + format!("https://{}/api/v1/version", ORIGIN), + None, + 200, + "{}", + vec![], + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + let (auth_source, _) = ConfigSourceMock::new("auth.json"); + let (conf_source, conf_removed) = ConfigSourceMock::new("config.json"); + config.borrow_mut().set_auth_config_source(auth_source); + config.borrow_mut().set_config_source(conf_source); + + let mut forgejo = build_forgejo(&io_mock, config, http_downloader); + + assert!( + forgejo + .authorize_o_auth_interactively(ORIGIN, Some(MESSAGE)) + .unwrap() + .unwrap() + ); + assert_eq!( + *conf_removed.borrow(), + vec![format!("forgejo-token.{}", ORIGIN)] + ); } #[test] -#[ignore = "requires getIOMock/getHttpDownloaderMock and getMockBuilder mocks of Config/JsonConfigSource with expects()/willReturn(); no mocking infrastructure exists"] fn test_username_password_failure() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Token (hidden): ", ACCESS_TOKEN), + ], + false, + ) + .unwrap(); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![expect_full( + format!("https://{}/api/v1/version", ORIGIN), + None, + 404, + "", + vec![], + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + let (auth_source, _) = ConfigSourceMock::new("auth.json"); + config.borrow_mut().set_auth_config_source(auth_source); + + let mut forgejo = build_forgejo(&io_mock, config, http_downloader); + + assert!( + !forgejo + .authorize_o_auth_interactively(ORIGIN, None) + .unwrap() + .unwrap() + ); } diff --git a/crates/shirabe/tests/util/gitlab_test.rs b/crates/shirabe/tests/util/gitlab_test.rs index e7c8bf4..5a91415 100644 --- a/crates/shirabe/tests/util/gitlab_test.rs +++ b/crates/shirabe/tests/util/gitlab_test.rs @@ -1,17 +1,174 @@ //! ref: composer/tests/Composer/Test/Util/GitLabTest.php -// Both cases construct GitLab with a mocked IO/Config/JsonConfigSource and a mocked -// HttpDownloader to drive the username/password authentication flow. Mocking is not -// available, and a real HttpDownloader reaches curl_multi_init (todo!()). +use std::cell::RefCell; +use std::rc::Rc; + +use shirabe::config::{Config, ConfigSourceInterface}; +use shirabe::io::IOInterface; +use shirabe::io::io_interface; +use shirabe::util::GitLab; +use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler}; +use shirabe_php_shim::PhpMixed; + +use crate::config_stub::ConfigStubBuilder; +use crate::http_downloader_mock::{expect_full, get_http_downloader_mock}; +use crate::io_mock::{Expectation, IOMock, get_io_mock}; + +const USERNAME: &str = "username"; +const PASSWORD: &str = "password"; +const MESSAGE: &str = "mymessage"; +const ORIGIN: &str = "gitlab.com"; +const TOKEN: &str = "gitlabtoken"; +const REFRESHTOKEN: &str = "gitlabrefreshtoken"; + +// Mirrors GitLabTest::getAuthJsonMock: a JsonConfigSource whose getName() returns +// "auth.json" and whose addConfigSetting is a no-op (the PHP mock never stubs it). +#[derive(Debug)] +struct AuthJsonMock; + +impl ConfigSourceInterface for AuthJsonMock { + fn add_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _append: bool, + ) -> anyhow::Result<()> { + unreachable!() + } + fn insert_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _reference_name: &str, + _offset: i64, + ) -> anyhow::Result<()> { + unreachable!() + } + fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + 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<()> { + unreachable!() + } + fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn add_link(&mut self, _type: &str, _name: &str, _value: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_link(&mut self, _type: &str, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn get_name(&self) -> String { + "auth.json".to_string() + } +} + +fn set_up(io_mock: &Rc<RefCell<IOMock>>, config: &Rc<RefCell<Config>>) { + config + .borrow_mut() + .set_auth_config_source(Box::new(AuthJsonMock)); + let _ = io_mock; +} -#[ignore = "needs getIOMock/IOMock and getMockBuilder mocks for HttpDownloader/Config/JsonConfigSource (no mock infrastructure)"] #[test] fn test_username_password_authentication_flow() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::text(MESSAGE), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + ], + false, + ) + .unwrap(); + + let body = format!( + "{{\"access_token\": \"{}\", \"refresh_token\": \"{}\", \"token_type\": \"bearer\", \"expires_in\": 7200, \"created_at\": 0}}", + TOKEN, REFRESHTOKEN + ); + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![expect_full( + format!("http://{}/oauth/token", ORIGIN), + None, + 200, + body, + vec![], + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + set_up(&io_mock, &config); + + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + let mut gitlab = GitLab::new(io, config, None, Some(http_downloader)).unwrap(); + + assert!( + gitlab + .authorize_oauth_interactively("http", ORIGIN, Some(MESSAGE)) + .unwrap() + ); } -#[ignore = "needs getIOMock/IOMock and getMockBuilder mocks for HttpDownloader/Config/JsonConfigSource (no mock infrastructure)"] #[test] fn test_username_password_failure() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + ], + false, + ) + .unwrap(); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + set_up(&io_mock, &config); + + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + let mut gitlab = GitLab::new(io, config, None, Some(http_downloader)).unwrap(); + + let err = gitlab + .authorize_oauth_interactively("https", ORIGIN, None) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Invalid GitLab credentials 5 times in a row, aborting." + ); } diff --git a/crates/shirabe/tests/util/tls_helper_test.rs b/crates/shirabe/tests/util/tls_helper_test.rs index 4717282..53e39d6 100644 --- a/crates/shirabe/tests/util/tls_helper_test.rs +++ b/crates/shirabe/tests/util/tls_helper_test.rs @@ -1,13 +1,138 @@ //! ref: composer/tests/Composer/Test/Util/TlsHelperTest.php +use indexmap::IndexMap; +use shirabe::util::tls_helper::TlsHelper; +use shirabe_php_shim::PhpMixed; + +// Builds the `['subject' => ['commonName' => ..], 'extensions' => ['subjectAltName' => ..]]` +// certificate array used by the test, given the common name and the subjectAltName string. +fn certificate(common_name: &str, subject_alt_name: &str) -> PhpMixed { + let mut subject = IndexMap::new(); + subject.insert( + "commonName".to_string(), + PhpMixed::String(common_name.to_string()), + ); + let mut extensions = IndexMap::new(); + extensions.insert( + "subjectAltName".to_string(), + PhpMixed::String(subject_alt_name.to_string()), + ); + let mut cert = IndexMap::new(); + cert.insert("subject".to_string(), PhpMixed::Array(subject)); + cert.insert("extensions".to_string(), PhpMixed::Array(extensions)); + PhpMixed::Array(cert) +} + +/// ref: TlsHelperTest::dataCheckCertificateHost +fn data_check_certificate_host() -> Vec<(bool, &'static str, Vec<&'static str>)> { + vec![ + (true, "getcomposer.org", vec!["getcomposer.org"]), + ( + true, + "getcomposer.org", + vec!["getcomposer.org", "packagist.org"], + ), + ( + true, + "getcomposer.org", + vec!["packagist.org", "getcomposer.org"], + ), + (true, "foo.getcomposer.org", vec!["*.getcomposer.org"]), + (false, "xyz.foo.getcomposer.org", vec!["*.getcomposer.org"]), + ( + true, + "foo.getcomposer.org", + vec!["getcomposer.org", "*.getcomposer.org"], + ), + ( + true, + "foo.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + true, + "foo1.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + true, + "foo2.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + false, + "foo2.another.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + false, + "test.example.net", + vec!["**.example.net", "**.example.net"], + ), + ( + false, + "test.example.net", + vec!["t*t.example.net", "t*t.example.net"], + ), + ( + false, + "xyz.example.org", + vec!["*z.example.org", "*z.example.org"], + ), + ( + false, + "foo.bar.example.com", + vec!["foo.*.example.com", "foo.*.example.com"], + ), + (false, "example.com", vec!["example.*", "example.*"]), + (true, "localhost", vec!["localhost"]), + (false, "localhost", vec!["*"]), + (false, "localhost", vec!["local*"]), + (false, "example.net", vec!["*.net", "*.org", "ex*.net"]), + (true, "example.net", vec!["*.net", "*.org", "example.net"]), + ] +} + #[test] -#[ignore = "TlsHelper::check_certificate_host not implemented in crates/shirabe/src"] fn test_check_certificate_host() { - todo!() + for (expected_result, hostname, mut cert_names) in data_check_certificate_host() { + let expected_cn = cert_names.remove(0); + let subject_alt_name = if cert_names.is_empty() { + String::new() + } else { + format!("DNS:{}", cert_names.join(",DNS:")) + }; + let cert = certificate(expected_cn, &subject_alt_name); + + let mut found_cn: Option<String> = None; + let result = TlsHelper::check_certificate_host(&cert, hostname, &mut found_cn); + + if expected_result { + assert!(result, "hostname {hostname} should match"); + assert_eq!(found_cn.as_deref(), Some(expected_cn)); + } else { + assert!(!result, "hostname {hostname} should not match"); + assert_eq!(found_cn, None); + } + } } #[test] -#[ignore = "TlsHelper::get_certificate_names not implemented in crates/shirabe/src"] fn test_get_certificate_names() { - todo!() + let cert = certificate( + "example.net", + "DNS: example.com, IP: 127.0.0.1, DNS: getcomposer.org, Junk: blah, DNS: composer.example.org", + ); + + let names = TlsHelper::get_certificate_names(&cert).unwrap(); + + assert_eq!(names.cn, "example.net"); + assert_eq!( + names.san, + vec![ + "example.com".to_string(), + "getcomposer.org".to_string(), + "composer.example.org".to_string(), + ] + ); } |
