aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/util
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 17:02:11 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commit3498bb1ca00ab7d051d296b8d482bea987a00fa4 (patch)
treee10260e5816317f4547847e2a230ea1a87fb2448 /crates/shirabe/tests/util
parent291b43d132749a61918dca23acef1b639c5333a7 (diff)
downloadphp-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.rs183
-rw-r--r--crates/shirabe/tests/util/gitlab_test.rs171
-rw-r--r--crates/shirabe/tests/util/tls_helper_test.rs133
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(),
+ ]
+ );
}