diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-27 17:21:00 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-27 17:26:28 +0900 |
| commit | e98823e599eb375b30037cc714710e3309d927d1 (patch) | |
| tree | e1689ec164086798fc46aa6a58d1e914cf4873b2 /crates/shirabe/tests/util/github_test.rs | |
| parent | 20f620bdd0b5764ed2e9812dc0772f907b0d6f29 (diff) | |
| download | php-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/github_test.rs')
| -rw-r--r-- | crates/shirabe/tests/util/github_test.rs | 178 |
1 files changed, 171 insertions, 7 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()); } |
