aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/util
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-27 17:21:00 +0900
committernsfisis <nsfisis@gmail.com>2026-06-27 17:26:28 +0900
commite98823e599eb375b30037cc714710e3309d927d1 (patch)
treee1689ec164086798fc46aa6a58d1e914cf4873b2 /crates/shirabe/tests/util
parent20f620bdd0b5764ed2e9812dc0772f907b0d6f29 (diff)
downloadphp-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.gz
php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.zst
php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.zip
test: port Composer tests unblocked by mockall, add seams
Port 11 categories of previously-ignored Composer tests now reachable with the mockall crate: DownloadManager, VCS/Perforce/File downloaders, VersionSelector, PlatformRepository, Auditor, installer/FilesystemRepository, RootPackageLoader, util auth/http, commands, and Cache. Extract test seams additively on concrete structs as *Interface traits (Runtime, HhvmDetector, VersionGuesser, RepositorySet, Perforce, BinaryInstaller) plus mock-field seams (Cache, Filesystem); consumers take trait objects. Mocks are defined locally in the test crates via mockall::mock!, since automock-generated mocks are cfg(test)-gated and invisible across the integration-test boundary. dataProviders are ported in full; tests blocked by unported shims stay #[ignore] with documented reasons rather than reduced or weakened. Fix product bugs surfaced by the ports: - util/github: use the exception code, not the HTTP status, for 401/403 - advisory: serialize empty audit maps as [] to match PHP json_encode - repository/filesystem and downloader/file: fix RefCell double-borrow panics 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/github_test.rs178
-rw-r--r--crates/shirabe/tests/util/http_downloader_test.rs47
2 files changed, 216 insertions, 9 deletions
diff --git a/crates/shirabe/tests/util/github_test.rs b/crates/shirabe/tests/util/github_test.rs
index 5caf5c7..10b5dd7 100644
--- a/crates/shirabe/tests/util/github_test.rs
+++ b/crates/shirabe/tests/util/github_test.rs
@@ -1,17 +1,181 @@
//! ref: composer/tests/Composer/Test/Util/GitHubTest.php
-// Both cases construct GitHub 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::GitHub;
+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 PASSWORD: &str = "password";
+const MESSAGE: &str = "mymessage";
+const ORIGIN: &str = "github.com";
+
+// Records the config setting names a source has had removed, plus a fixed getName,
+// mirroring GitHubTest's JsonConfigSource mocks (getName -> "auth.json", and the
+// config source stubbing removeConfigSetting('github-oauth.<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_github(
+ io_mock: &Rc<RefCell<IOMock>>,
+ config: Rc<RefCell<Config>>,
+ http_downloader: Rc<RefCell<HttpDownloader>>,
+) -> GitHub {
+ let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone();
+ GitHub::new(io, config, None, Some(http_downloader)).unwrap()
+}
+
+// The PHP Config mock returns null for `get('github-expose-hostname')`, which is
+// falsy and skips the `hostname` process call. A real Config defaults that key to
+// true, so the stub seeds false to reproduce the mock's behaviour.
+fn build_config() -> Rc<RefCell<Config>> {
+ ConfigStubBuilder::new()
+ .with("github-expose-hostname", PhpMixed::Bool(false))
+ .build_shared()
+}
#[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("Token (hidden): ", PASSWORD),
+ ],
+ false,
+ )
+ .unwrap();
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![expect_full(
+ format!("https://api.{}/", ORIGIN),
+ None,
+ 200,
+ "{}",
+ vec![],
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let config = build_config();
+ 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 github = build_github(&io_mock, config, http_downloader);
+
+ assert!(
+ github
+ .authorize_oauth_interactively(ORIGIN, Some(MESSAGE))
+ .unwrap()
+ );
+ assert_eq!(
+ *conf_removed.borrow(),
+ vec![format!("github-oauth.{}", 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("Token (hidden): ", PASSWORD)], false)
+ .unwrap();
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![expect_full(
+ format!("https://api.{}/", ORIGIN),
+ None,
+ 401,
+ "",
+ vec![],
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let config = build_config();
+ let (auth_source, _) = ConfigSourceMock::new("auth.json");
+ config.borrow_mut().set_auth_config_source(auth_source);
+
+ let mut github = build_github(&io_mock, config, http_downloader);
+
+ assert!(!github.authorize_oauth_interactively(ORIGIN, None).unwrap());
}
diff --git a/crates/shirabe/tests/util/http_downloader_test.rs b/crates/shirabe/tests/util/http_downloader_test.rs
index 59807a9..078fd57 100644
--- a/crates/shirabe/tests/util/http_downloader_test.rs
+++ b/crates/shirabe/tests/util/http_downloader_test.rs
@@ -4,16 +4,59 @@ use std::cell::RefCell;
use std::rc::Rc;
use indexmap::IndexMap;
+use shirabe::config::Config;
+use shirabe::downloader::TransportException;
use shirabe::io::IOInterface;
use shirabe::io::buffer_io::BufferIO;
+use shirabe::io::io_interface;
+use shirabe::util::Platform;
use shirabe::util::http_downloader::HttpDownloader;
use shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL;
use shirabe_php_shim::{PHP_EOL, PhpMixed};
+use crate::config_stub::ConfigStubBuilder;
+use crate::io_mock::{Expectation, get_io_mock};
+
+// PHP performs a live HTTP get to assert the URL's user:pass is captured via
+// setAuthentication. The credential capture happens in `add_job`, before any
+// network I/O, so COMPOSER_DISABLE_NETWORK short-circuits the actual request
+// (yielding a non-200 TransportException, as PHP's live 404 would) while the
+// setAuthentication side effect still runs and is verified through the IOMock.
#[test]
-#[ignore = "asserts IOInterface mock ->expects()->method('setAuthentication')->with(...) and performs a live HTTP get; no mock infrastructure exists"]
+#[serial_test::serial]
fn test_capture_authentication_params_from_url() {
- todo!()
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(
+ vec![Expectation::auth(
+ "github.com",
+ "user",
+ Some("pass".to_string()),
+ )],
+ false,
+ )
+ .unwrap();
+
+ // The PHP Config mock returns [] for github-domains/gitlab-domains.
+ let config: Rc<RefCell<Config>> = ConfigStubBuilder::new()
+ .with("github-domains", PhpMixed::Array(IndexMap::new()))
+ .with("gitlab-domains", PhpMixed::Array(IndexMap::new()))
+ .build_shared();
+
+ let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone();
+
+ Platform::put_env("COMPOSER_DISABLE_NETWORK", "1");
+ let mut fs = HttpDownloader::new(io, config, IndexMap::new(), false);
+ Platform::clear_env("COMPOSER_DISABLE_NETWORK");
+
+ if let Err(e) = fs.get(
+ "https://user:pass@github.com/composer/composer/404",
+ IndexMap::new(),
+ ) && let Some(te) = e.downcast_ref::<TransportException>()
+ {
+ assert_ne!(200, te.get_code());
+ }
}
#[test]