diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 15:37:21 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-26 00:20:05 +0900 |
| commit | 3a883e6912e642a1bcfe68336007eae018207308 (patch) | |
| tree | 2750519e1bbaaf8caa493bcf1eb15efb143280d9 /crates/shirabe | |
| parent | d4cdccb8de8758bd46a12283f8df90e020327b99 (diff) | |
| download | php-shirabe-3a883e6912e642a1bcfe68336007eae018207308.tar.gz php-shirabe-3a883e6912e642a1bcfe68336007eae018207308.tar.zst php-shirabe-3a883e6912e642a1bcfe68336007eae018207308.zip | |
test: port 35 auth/installer/io/zip/bitbucket tests; implement date_create
Port auth_helper (14), library_installer (8), console_io (7), zip_downloader (3),
git_bitbucket_driver (3) tests. Implement date_create/strtotime for the ISO8601/
RFC3339/relative formats Composer uses (unknown input -> None, no silent guess).
Fix production bugs: Question::is_assoc list-vs-assoc, auth_helper gitlab-domains
list handling, LibraryInstaller RefCell double-borrow, ZipArchive::extract_to
ErrorException propagation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe')
| -rw-r--r-- | crates/shirabe/src/downloader/zip_downloader.rs | 24 | ||||
| -rw-r--r-- | crates/shirabe/src/installer/library_installer.rs | 26 | ||||
| -rw-r--r-- | crates/shirabe/src/package/handle.rs | 9 | ||||
| -rw-r--r-- | crates/shirabe/src/util/auth_helper.rs | 16 | ||||
| -rw-r--r-- | crates/shirabe/tests/common/downloader_stub.rs | 129 | ||||
| -rw-r--r-- | crates/shirabe/tests/downloader/zip_downloader_test.rs | 186 | ||||
| -rw-r--r-- | crates/shirabe/tests/installer/library_installer_test.rs | 168 | ||||
| -rw-r--r-- | crates/shirabe/tests/installer/main.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/tests/io/console_io_test.rs | 165 | ||||
| -rw-r--r-- | crates/shirabe/tests/repository/vcs/git_bitbucket_driver_test.rs | 341 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/auth_helper_test.rs | 569 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/main.rs | 2 |
12 files changed, 1417 insertions, 220 deletions
diff --git a/crates/shirabe/src/downloader/zip_downloader.rs b/crates/shirabe/src/downloader/zip_downloader.rs index bea2b20..80ce6f9 100644 --- a/crates/shirabe/src/downloader/zip_downloader.rs +++ b/crates/shirabe/src/downloader/zip_downloader.rs @@ -332,7 +332,7 @@ impl ZipDownloader { } } - let extract_result = zip_archive.extract_to(path); + let extract_result = zip_archive.extract_to(path)?; if extract_result { zip_archive.close(); @@ -393,6 +393,28 @@ impl ZipDownloader { ), } } + + /// For testing only. Mirrors the test's `setPrivateProperty('hasZipArchive', ...)` reflection on + /// the `ZipDownloader::$hasZipArchive` static. + pub fn __set_has_zip_archive(value: Option<bool>) { + *HAS_ZIP_ARCHIVE.lock().unwrap() = value; + } + + /// For testing only. Mirrors the test's `setPrivateProperty('isWindows', ...)` reflection. + pub fn __set_is_windows(value: Option<bool>) { + *IS_WINDOWS.lock().unwrap() = value; + } + + /// For testing only. Mirrors the test's `setPrivateProperty('unzipCommands', ...)` reflection. + pub fn __set_unzip_commands(value: Option<Vec<Vec<String>>>) { + *UNZIP_COMMANDS.lock().unwrap() = value; + } + + /// For testing only. Mirrors the test's `setPrivateProperty('zipArchiveObject', $zipArchive, $obj)` + /// reflection on the instance's `$zipArchiveObject` property. + pub fn __set_zip_archive_object(&mut self, value: Option<ZipArchive>) { + self.zip_archive_object = value; + } } impl ArchiveDownloader for ZipDownloader { diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs index d2f222e..d716814 100644 --- a/crates/shirabe/src/installer/library_installer.rs +++ b/crates/shirabe/src/installer/library_installer.rs @@ -63,21 +63,23 @@ impl LibraryInstaller { Some("/"), ); let binary_installer = binary_installer.unwrap_or_else(|| { - BinaryInstaller::new( - io.clone(), - rtrim( - &composer_ref - .get_config() - .borrow_mut() - .get_str("bin-dir") - .unwrap_or_default(), - Some("/"), - ), - composer_ref + let bin_dir = rtrim( + &composer_ref .get_config() .borrow_mut() - .get_str("bin-compat") + .get_str("bin-dir") .unwrap_or_default(), + Some("/"), + ); + let bin_compat = composer_ref + .get_config() + .borrow_mut() + .get_str("bin-compat") + .unwrap_or_default(); + BinaryInstaller::new( + io.clone(), + bin_dir, + bin_compat, Some(filesystem.clone()), Some(vendor_dir.clone()), ) diff --git a/crates/shirabe/src/package/handle.rs b/crates/shirabe/src/package/handle.rs index 8a6ed83..9405692 100644 --- a/crates/shirabe/src/package/handle.rs +++ b/crates/shirabe/src/package/handle.rs @@ -1231,6 +1231,15 @@ macro_rules! impl_real_package_test_setters { .expect("real package handle invariant") .set_dist_sha1_checksum(sha1checksum); } + + /// For testing only: mirrors PHP `Package::setTargetDir`. + pub fn __set_target_dir(&self, target_dir: Option<String>) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_target_dir(target_dir); + } } }; } diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs index 0b1620d..f386b1e 100644 --- a/crates/shirabe/src/util/auth_helper.rs +++ b/crates/shirabe/src/util/auth_helper.rs @@ -534,14 +534,14 @@ impl AuthHelper { true, ) && in_array( PhpMixed::String(origin.to_string()), - &PhpMixed::List( - self.config - .borrow_mut() - .get("gitlab-domains") - .as_array() - .map(|a| a.values().map(|v| v.clone()).collect()) - .unwrap_or_default(), - ), + &PhpMixed::List({ + let gitlab_domains = self.config.borrow_mut().get("gitlab-domains"); + match &gitlab_domains { + PhpMixed::List(l) => l.clone(), + PhpMixed::Array(a) => a.values().cloned().collect(), + _ => vec![], + } + }), true, ) { if password == "oauth2" { diff --git a/crates/shirabe/tests/common/downloader_stub.rs b/crates/shirabe/tests/common/downloader_stub.rs new file mode 100644 index 0000000..0c4432b --- /dev/null +++ b/crates/shirabe/tests/common/downloader_stub.rs @@ -0,0 +1,129 @@ +//! No-op DownloaderInterface stub for installer tests. +//! +//! PHP mocks `Composer\Downloader\DownloadManager` directly and asserts its +//! `install`/`update`/`remove` calls. The Rust DownloadManager is a concrete type +//! that dispatches to a registered DownloaderInterface, so the equivalent stub +//! lives one level down: a downloader registered under some dist type that records +//! the calls it receives and resolves to null, the same way the PHP mock returns +//! `\React\Promise\resolve(null)`. +#![allow(dead_code)] + +use std::cell::RefCell; +use std::rc::Rc; + +use shirabe::downloader::DownloaderInterface; +use shirabe::package::PackageInterfaceHandle; +use shirabe_php_shim::PhpMixed; + +/// One recorded downloader operation, capturing the package pretty-names and the +/// target path so tests can assert what the LibraryInstaller forwarded. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DownloaderCall { + Install { + package: String, + path: String, + }, + Update { + initial: String, + target: String, + path: String, + }, + Remove { + package: String, + path: String, + }, +} + +#[derive(Debug, Default)] +pub struct DownloaderStub { + calls: Rc<RefCell<Vec<DownloaderCall>>>, +} + +impl DownloaderStub { + pub fn new() -> Self { + Self::default() + } + + /// Shared handle to the recorded call log, so tests can inspect it after the + /// stub has been moved into the DownloadManager. + pub fn calls(&self) -> Rc<RefCell<Vec<DownloaderCall>>> { + self.calls.clone() + } +} + +#[async_trait::async_trait(?Send)] +impl DownloaderInterface for DownloaderStub { + fn get_installation_source(&self) -> String { + "dist".to_string() + } + + async fn download( + &mut self, + _package: PackageInterfaceHandle, + _path: &str, + _prev_package: Option<PackageInterfaceHandle>, + _output: bool, + ) -> anyhow::Result<Option<PhpMixed>> { + Ok(None) + } + + async fn prepare( + &mut self, + _type: &str, + _package: PackageInterfaceHandle, + _path: &str, + _prev_package: Option<PackageInterfaceHandle>, + ) -> anyhow::Result<Option<PhpMixed>> { + Ok(None) + } + + async fn install( + &mut self, + package: PackageInterfaceHandle, + path: &str, + _output: bool, + ) -> anyhow::Result<Option<PhpMixed>> { + self.calls.borrow_mut().push(DownloaderCall::Install { + package: package.get_pretty_name(), + path: path.to_string(), + }); + Ok(None) + } + + async fn update( + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, + ) -> anyhow::Result<Option<PhpMixed>> { + self.calls.borrow_mut().push(DownloaderCall::Update { + initial: initial.get_pretty_name(), + target: target.get_pretty_name(), + path: path.to_string(), + }); + Ok(None) + } + + async fn remove( + &mut self, + package: PackageInterfaceHandle, + path: &str, + _output: bool, + ) -> anyhow::Result<Option<PhpMixed>> { + self.calls.borrow_mut().push(DownloaderCall::Remove { + package: package.get_pretty_name(), + path: path.to_string(), + }); + Ok(None) + } + + async fn cleanup( + &mut self, + _type: &str, + _package: PackageInterfaceHandle, + _path: &str, + _prev_package: Option<PackageInterfaceHandle>, + ) -> anyhow::Result<Option<PhpMixed>> { + Ok(None) + } +} diff --git a/crates/shirabe/tests/downloader/zip_downloader_test.rs b/crates/shirabe/tests/downloader/zip_downloader_test.rs index 0bffa89..055e98f 100644 --- a/crates/shirabe/tests/downloader/zip_downloader_test.rs +++ b/crates/shirabe/tests/downloader/zip_downloader_test.rs @@ -1,32 +1,79 @@ //! ref: composer/tests/Composer/Test/Downloader/ZipDownloaderTest.php +use std::cell::RefCell; +use std::rc::Rc; + +use serial_test::serial; +use shirabe::config::Config; +use shirabe::downloader::ArchiveDownloader; +use shirabe::downloader::zip_downloader::ZipDownloader; +use shirabe::io::IOInterface; +use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; +use shirabe::util::HttpDownloader; +use shirabe::util::ProcessExecutor; use shirabe::util::filesystem::Filesystem; +use shirabe_php_shim::{ZipArchive, ZipArchiveMock}; +use shirabe_semver::VersionParser; use tempfile::TempDir; +use crate::io_stub::IOStub; + +fn run<F: std::future::Future>(future: F) -> F::Output { + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(future) +} + struct SetUp { test_dir: TempDir, + io: Rc<RefCell<dyn IOInterface>>, + config: Rc<RefCell<Config>>, + http_downloader: Rc<RefCell<HttpDownloader>>, + package: PackageInterfaceHandle, filename: std::path::PathBuf, } +/// ref: ZipDownloaderTest::setUp. +/// +/// The PHP test mocks IOInterface/Config/HttpDownloader/PackageInterface via PHPUnit. Here the +/// IO/Config use the existing stubs, the HttpDownloader is built as a mock (no network is touched on +/// the `extract` path exercised by the ported tests), and the package is a real CompletePackage whose +/// `getName()` is `test/pkg`, matching the PHP mock's `->method('getName')->willReturn('test/pkg')`. fn set_up() -> SetUp { let test_dir = TempDir::new().unwrap(); - // The IO/Config/HttpDownloader/Package mocks are not ported; HttpDownloader construction - // additionally reaches curl_multi_init (todo!()). - let () = todo!(); - #[allow(unreachable_code)] - { - let filename = test_dir.path().join("composer-test.zip"); - std::fs::write(&filename, "zip").unwrap(); - SetUp { test_dir, filename } + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(IOStub::new())); + let config = Rc::new(RefCell::new(Config::new(false, None))); + let dl_config = Rc::new(RefCell::new(Config::new(false, None))); + let http_downloader = Rc::new(RefCell::new(HttpDownloader::__new_mock( + io.clone(), + dl_config, + ))); + + let norm_version = VersionParser.normalize("1.0.0", None).unwrap(); + let package: PackageInterfaceHandle = + CompletePackageHandle::new("test/pkg".to_string(), norm_version, "1.0.0".to_string()) + .into(); + + let filename = test_dir.path().join("composer-test.zip"); + std::fs::write(&filename, "zip").unwrap(); + + SetUp { + test_dir, + io, + config, + http_downloader, + package, + filename, } } fn tear_down(test_dir: &std::path::Path) { let mut fs = Filesystem::new(None); fs.remove_directory(test_dir).unwrap(); - // setPrivateProperty('hasZipArchive', null) resets a ZipDownloader static via reflection; - // the static is not reachable from a test here. - todo!() + // setPrivateProperty('hasZipArchive', null) + ZipDownloader::__set_has_zip_archive(None); } struct TearDown { @@ -45,76 +92,135 @@ impl Drop for TearDown { } } -// These construct a ZipDownloader with a mocked IO/HttpDownloader/ProcessExecutor and rely -// on ZipArchive extraction (todo!() in the php-shim) plus mocked unzip behaviour. -#[ignore = "requires PHPUnit mocks of IOInterface/Config/PackageInterface; set_up() is todo!() and real HttpDownloader reaches curl_multi_init todo!(); no mocking framework"] +fn make_downloader(set_up: &SetUp) -> ZipDownloader { + let filesystem = Rc::new(RefCell::new(Filesystem::new(None))); + let process = Rc::new(RefCell::new(ProcessExecutor::new(Some(set_up.io.clone())))); + ZipDownloader::new( + set_up.io.clone(), + set_up.config.clone(), + set_up.http_downloader.clone(), + None, + None, + filesystem, + process, + ) +} + +// The system-unzip / non-windows-fallback paths route through ProcessExecutor::execute_async, whose +// mock branch is an unimplemented todo!() (no Process mock seam exists in the external-packages +// crate). The PHP tests below mock Process/ProcessExecutor::executeAsync, which is not reproducible +// here, so they remain ignored. +// +// testErrorMessages drives a real HttpDownloader + Loop (curl_multi_init todo!()), also out of reach. + +#[ignore = "drives a real HttpDownloader + Loop (download/install), which reaches curl_multi_init todo!()"] #[test] fn test_error_messages() { let set_up = set_up(); let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); + let _ = (&set_up.io, &set_up.config, &set_up.http_downloader); todo!() } -#[ignore = "requires setPrivateProperty reflection on hasZipArchive/zipArchiveObject, a MockedZipDownloader subclass, and a getMockBuilder('ZipArchive') mock; no mocking/reflection framework"] #[test] +#[serial] fn test_zip_archive_only_failed() { let set_up = set_up(); let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); - todo!() + + ZipDownloader::__set_has_zip_archive(Some(true)); + let mut downloader = make_downloader(&set_up); + let zip_archive = ZipArchive::__mock(ZipArchiveMock { + open: Ok(()), + count: 0, + extract_to: Ok(false), + }); + downloader.__set_zip_archive_object(Some(zip_archive)); + + let filename = set_up.filename.to_string_lossy().into_owned(); + let result = run(downloader.extract(set_up.package.clone(), &filename, "vendor/dir")); + + let e = result.expect_err("expected RuntimeException"); + assert!( + e.to_string() + .contains("There was an error extracting the ZIP file"), + "got: {e}" + ); } -#[ignore = "requires setPrivateProperty reflection on hasZipArchive/zipArchiveObject, a MockedZipDownloader subclass, and a getMockBuilder('ZipArchive') mock throwing ErrorException; no mocking/reflection framework"] #[test] +#[serial] fn test_zip_archive_extract_only_failed() { let set_up = set_up(); let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); - todo!() + + ZipDownloader::__set_has_zip_archive(Some(true)); + let mut downloader = make_downloader(&set_up); + let zip_archive = ZipArchive::__mock(ZipArchiveMock { + open: Ok(()), + count: 0, + extract_to: Err("Not a directory".to_string()), + }); + downloader.__set_zip_archive_object(Some(zip_archive)); + + let filename = set_up.filename.to_string_lossy().into_owned(); + let result = run(downloader.extract(set_up.package.clone(), &filename, "vendor/dir")); + + let e = result.expect_err("expected RuntimeException"); + assert!( + e.to_string().contains( + "The archive for \"test/pkg\" may contain identical file names with different \ + capitalization (which fails on case insensitive filesystems): Not a directory" + ), + "got: {e}" + ); } -#[ignore = "requires setPrivateProperty reflection on hasZipArchive/zipArchiveObject, a MockedZipDownloader subclass, and a getMockBuilder('ZipArchive') mock; no mocking/reflection framework"] #[test] +#[serial] fn test_zip_archive_only_good() { let set_up = set_up(); let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); - todo!() + + ZipDownloader::__set_has_zip_archive(Some(true)); + let mut downloader = make_downloader(&set_up); + let zip_archive = ZipArchive::__mock(ZipArchiveMock { + open: Ok(()), + count: 0, + extract_to: Ok(true), + }); + downloader.__set_zip_archive_object(Some(zip_archive)); + + let filename = set_up.filename.to_string_lossy().into_owned(); + let result = run(downloader.extract(set_up.package.clone(), &filename, "vendor/dir")); + + result.expect("extract should succeed"); } -#[ignore = "requires setPrivateProperty reflection on isWindows/hasZipArchive/unzipCommands, a MockedZipDownloader subclass, and getMockBuilder mocks of Process/ProcessExecutor::executeAsync; no mocking/reflection framework"] +#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] #[test] fn test_system_unzip_only_failed() { - let set_up = set_up(); - let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); + let _ = set_up(); todo!() } -#[ignore = "requires setPrivateProperty reflection on isWindows/hasZipArchive/unzipCommands, a MockedZipDownloader subclass, and getMockBuilder mocks of Process/ProcessExecutor::executeAsync; no mocking/reflection framework"] +#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] #[test] fn test_system_unzip_only_good() { - let set_up = set_up(); - let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); + let _ = set_up(); todo!() } -#[ignore = "requires setPrivateProperty reflection on isWindows/hasZipArchive/zipArchiveObject, a MockedZipDownloader subclass, and getMockBuilder mocks of Process/ProcessExecutor/ZipArchive; no mocking/reflection framework"] +#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] #[test] fn test_non_windows_fallback_good() { - let set_up = set_up(); - let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); + let _ = set_up(); todo!() } -#[ignore = "requires setPrivateProperty reflection on isWindows/hasZipArchive/zipArchiveObject, a MockedZipDownloader subclass, and getMockBuilder mocks of Process/ProcessExecutor/ZipArchive; no mocking/reflection framework"] +#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] #[test] fn test_non_windows_fallback_failed() { - let set_up = set_up(); - let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); - let _ = (&set_up.test_dir, &set_up.filename); + let _ = set_up(); todo!() } diff --git a/crates/shirabe/tests/installer/library_installer_test.rs b/crates/shirabe/tests/installer/library_installer_test.rs index 62a2c0c..2e4883e 100644 --- a/crates/shirabe/tests/installer/library_installer_test.rs +++ b/crates/shirabe/tests/installer/library_installer_test.rs @@ -16,12 +16,21 @@ use shirabe::installer::{InstallerInterface, LibraryInstaller}; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::repository::InstalledArrayRepository; +use shirabe::repository::RepositoryInterface; use shirabe::repository::WritableRepositoryInterface; use shirabe::util::filesystem::Filesystem; use shirabe_php_shim::PhpMixed; +use crate::downloader_stub::{DownloaderCall, DownloaderStub}; use crate::test_case::get_package; +fn run<F: std::future::Future>(future: F) -> F::Output { + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(future) +} + /// Mirror of setUp(): builds the Composer/Config over temp root/vendor/bin dirs /// plus a real DownloadManager and a NullIO. The `_composer_rc` keeps the inner /// Rc alive for the duration of the test since LibraryInstaller only holds a weak @@ -33,6 +42,9 @@ struct SetUp { io: Rc<RefCell<dyn IOInterface>>, composer: PartialComposerWeakHandle, fs: Filesystem, + /// Recorded downloader operations forwarded by the LibraryInstaller, mirroring the + /// PHP DownloadManager mock's expectations on install/update/remove. + downloader_calls: Rc<RefCell<Vec<DownloaderCall>>>, _composer_rc: ComposerHandle, } @@ -65,7 +77,10 @@ fn set_up() -> SetUp { let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); - let dm = DownloadManager::new(io.clone(), false, None); + let mut dm = DownloadManager::new(io.clone(), false, None); + let downloader = DownloaderStub::new(); + let downloader_calls = downloader.calls(); + dm.set_downloader("fake", Rc::new(RefCell::new(downloader))); let dm_rc = Rc::new(RefCell::new(dm)); let composer_rc = Rc::new(RefCell::new(PartialOrFullComposer::new_full())); @@ -82,16 +97,26 @@ fn set_up() -> SetUp { io, composer: weak, fs, + downloader_calls, _composer_rc: composer, } } +/// Builds a `dist`-installable test package backed by the `fake` downloader stub, so the +/// real DownloadManager dispatches install/update/remove to the recording stub instead of +/// erroring on a package with no installation source. +fn get_installable_package(name: &str, version: &str) -> shirabe::package::PackageInterfaceHandle { + let package = get_package(name, version); + package.set_installation_source(Some("dist".to_string())); + package.set_dist_type(Some("fake".to_string())); + package +} + fn tear_down(setup: &mut SetUp) { let root = setup.root.path().to_path_buf(); setup.fs.remove_directory(&root).ok(); } -#[ignore] #[test] fn test_installer_creation_should_not_create_vendor_directory() { let mut setup = set_up(); @@ -103,7 +128,6 @@ fn test_installer_creation_should_not_create_vendor_directory() { tear_down(&mut setup); } -#[ignore] #[test] fn test_installer_creation_should_not_create_bin_directory() { let mut setup = set_up(); @@ -115,7 +139,6 @@ fn test_installer_creation_should_not_create_bin_directory() { tear_down(&mut setup); } -#[ignore] #[test] fn test_is_installed() { let mut setup = set_up(); @@ -142,24 +165,129 @@ fn test_is_installed() { } #[test] -#[ignore = "requires PHPUnit mock of DownloadManager (expects(once)->method('install')->will(resolve(null))) and InstalledRepositoryInterface (expects(once)->addPackage); a real DownloadManager would attempt a download"] fn test_install() { - todo!() + let mut setup = set_up(); + let mut library = + LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); + let package = get_installable_package("some/package", "1.0.0"); + + // PHP asserts the DownloadManager mock's install() is called once with + // ($package, vendorDir/some/package); here the recording downloader stub + // captures the forwarded call instead. + let mut repository = InstalledArrayRepository::new().unwrap(); + + run(library.install(&mut repository, package.clone())).unwrap(); + + assert_eq!( + vec![DownloaderCall::Install { + package: "some/package".to_string(), + path: format!("{}/some/package", setup.vendor_dir), + }], + *setup.downloader_calls.borrow() + ); + // PHP asserts repository->addPackage was called once with $package. + assert!(repository.has_package(package)); + + assert!( + std::path::Path::new(&setup.vendor_dir).exists(), + "Vendor dir should be created" + ); + assert!( + std::path::Path::new(&setup.bin_dir).exists(), + "Bin dir should be created" + ); + + tear_down(&mut setup); } #[test] -#[ignore = "requires PHPUnit mocks (Filesystem::rename, DownloadManager::update, repository hasPackage/add/remove with onConsecutiveCalls) and Package::setTargetDir, which is not exposed on PackageInterfaceHandle"] fn test_update() { - todo!() + let mut setup = set_up(); + + let initial = get_installable_package("vendor/package1", "1.0.0"); + let target = get_installable_package("vendor/package1", "2.0.0"); + + initial.__set_target_dir(Some("oldtarget".to_string())); + target.__set_target_dir(Some("newtarget".to_string())); + + // PHP mocks Filesystem::rename; here a real Filesystem renames the actual oldtarget + // dir, so it must exist first. The install path embeds the pretty-name twice because + // the package is named vendor/package1 and lives under vendorDir. + let old_target_dir = format!("{}/vendor/package1/oldtarget", setup.vendor_dir); + let new_target_dir = format!("{}/vendor/package1/newtarget", setup.vendor_dir); + fs::create_dir_all(&old_target_dir).unwrap(); + + let mut repository = InstalledArrayRepository::new().unwrap(); + repository.add_package(initial.clone()).unwrap(); + + // The default Filesystem is fine; the LibraryInstaller's own filesystem performs the rename. + let mut library = + LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); + + run(library.update(&mut repository, initial.clone(), target.clone())).unwrap(); + + assert!( + std::path::Path::new(&new_target_dir).exists(), + "oldtarget should have been renamed to newtarget" + ); + assert!(!std::path::Path::new(&old_target_dir).exists()); + + assert_eq!( + vec![DownloaderCall::Update { + initial: "vendor/package1".to_string(), + target: "vendor/package1".to_string(), + path: new_target_dir.clone(), + }], + *setup.downloader_calls.borrow() + ); + assert!(!repository.has_package(initial.clone())); + assert!(repository.has_package(target.clone())); + + assert!( + std::path::Path::new(&setup.vendor_dir).exists(), + "Vendor dir should be created" + ); + assert!( + std::path::Path::new(&setup.bin_dir).exists(), + "Bin dir should be created" + ); + + // Updating again, with the initial package no longer installed, fails. + assert!(run(library.update(&mut repository, initial, target)).is_err()); + + tear_down(&mut setup); } #[test] -#[ignore = "requires PHPUnit mock of DownloadManager (expects(once)->method('remove')->will(resolve(null))) and InstalledRepositoryInterface (hasPackage onConsecutiveCalls, removePackage)"] fn test_uninstall() { - todo!() + let mut setup = set_up(); + let mut library = + LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); + let package = get_installable_package("vendor/pkg", "1.0.0"); + + // PHP mocks hasPackage to return (true, false) over two calls; a real repository + // seeded with the package reproduces this naturally: present, then absent after + // the first uninstall removes it. + let mut repository = InstalledArrayRepository::new().unwrap(); + repository.add_package(package.clone()).unwrap(); + + run(library.uninstall(&mut repository, package.clone())).unwrap(); + + assert_eq!( + vec![DownloaderCall::Remove { + package: "vendor/pkg".to_string(), + path: format!("{}/vendor/pkg", setup.vendor_dir), + }], + *setup.downloader_calls.borrow() + ); + assert!(!repository.has_package(package.clone())); + + // Uninstalling again, with the package no longer installed, fails. + assert!(run(library.uninstall(&mut repository, package)).is_err()); + + tear_down(&mut setup); } -#[ignore] #[test] fn test_get_install_path_without_target_dir() { let mut setup = set_up(); @@ -176,9 +304,23 @@ fn test_get_install_path_without_target_dir() { } #[test] -#[ignore = "Package::setTargetDir is not exposed on PackageInterfaceHandle, so the target-dir cannot be set on the test package"] fn test_get_install_path_with_target_dir() { - todo!() + let mut setup = set_up(); + let mut library = + LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); + let package = get_package("Foo/Bar", "1.0.0"); + package.__set_target_dir(Some("Some/Namespace".to_string())); + + assert_eq!( + format!( + "{}/{}/Some/Namespace", + setup.vendor_dir, + package.get_pretty_name() + ), + library.get_install_path(package).unwrap() + ); + + tear_down(&mut setup); } #[test] diff --git a/crates/shirabe/tests/installer/main.rs b/crates/shirabe/tests/installer/main.rs index f5b8c32..931e70b 100644 --- a/crates/shirabe/tests/installer/main.rs +++ b/crates/shirabe/tests/installer/main.rs @@ -1,3 +1,5 @@ +#[path = "../common/downloader_stub.rs"] +mod downloader_stub; #[path = "../common/test_case.rs"] mod test_case; diff --git a/crates/shirabe/tests/io/console_io_test.rs b/crates/shirabe/tests/io/console_io_test.rs index 3c8242a..f96aec1 100644 --- a/crates/shirabe/tests/io/console_io_test.rs +++ b/crates/shirabe/tests/io/console_io_test.rs @@ -1,17 +1,28 @@ //! ref: composer/tests/Composer/Test/IO/ConsoleIOTest.php -// These mock Symfony's InputInterface/OutputInterface/HelperSet (and QuestionHelper) to -// drive ConsoleIO; those console abstractions are not ported. +// PHP drives ConsoleIO with PHPUnit mocks of Symfony's InputInterface/OutputInterface/HelperSet +// (and QuestionHelper). There is no mocking framework here, so the mocks are reproduced with real, +// side-effect-free Symfony console types: +// * `expects()->method('write')->with(...)` call/argument expectations are replaced by reading +// back what a real `BufferedOutput` collected and asserting on it. +// * `isInteractive` consecutive return values are reproduced by toggling a real `ArrayInput`'s +// interactive flag between calls. +// * QuestionHelper `ask`/`get` expectations are replaced by feeding a prepared answer through a +// `php://memory` input stream (exactly as StrictConfirmationQuestionTest does) and asserting on +// the returned value. use indexmap::IndexMap; use shirabe::io::ConsoleIO; use shirabe::io::IOInterfaceImmutable; use shirabe::io::IOInterfaceMutable; +use shirabe::io::io_interface::NORMAL; use shirabe_external_packages::symfony::console::helper::QuestionHelper; use shirabe_external_packages::symfony::console::input::array_input::ArrayInput; use shirabe_external_packages::symfony::console::input::input_interface::InputInterface; +use shirabe_external_packages::symfony::console::input::streamable_input_interface::StreamableInputInterface; use shirabe_external_packages::symfony::console::output::buffered_output::BufferedOutput; use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface; +use shirabe_php_shim::PhpMixed; use std::cell::RefCell; use std::rc::Rc; @@ -26,58 +37,176 @@ fn make_console_io() -> ConsoleIO { ConsoleIO::new(input, output, QuestionHelper::default()) } -#[ignore = "requires a PHPUnit mock of InputInterface::isInteractive with willReturnOnConsecutiveCalls; no mocking framework"] +/// Opens a `php://memory` stream pre-filled with `input`, mirroring +/// StrictConfirmationQuestionTest::getInputStream. +fn get_input_stream(input: &str) -> shirabe_php_shim::PhpResource { + let stream = shirabe_php_shim::php_fopen_resource("php://memory", "r+"); + shirabe_php_shim::fwrite_resource(&stream, input); + shirabe_php_shim::rewind(&stream); + stream +} + +/// Builds a ConsoleIO whose interactive input stream yields `answer`, plus a handle on the +/// `BufferedOutput` so the prompt can be inspected. The input is an `ArrayInput` (interactive by +/// default) with the answer stream attached, exactly as the question-helper tests set things up. +fn make_console_io_with_answer(answer: &str) -> (ConsoleIO, Rc<RefCell<BufferedOutput>>) { + let mut array_input = ArrayInput::new(vec![], None).unwrap(); + array_input.set_stream(get_input_stream(answer)); + let input: Rc<RefCell<dyn InputInterface>> = Rc::new(RefCell::new(array_input)); + let buffered = Rc::new(RefCell::new(BufferedOutput::new(None, false, None))); + let output: Rc<RefCell<dyn OutputInterface>> = buffered.clone(); + ( + ConsoleIO::new(input, output, QuestionHelper::default()), + buffered, + ) +} + #[test] fn test_is_interactive() { - todo!() + // PHP mocks isInteractive to return true then false on consecutive calls. A real ArrayInput + // exposes a mutable interactive flag, so toggling it between the two asserts is equivalent. + let array_input = ArrayInput::new(vec![], None).unwrap(); + let input: Rc<RefCell<dyn InputInterface>> = Rc::new(RefCell::new(array_input)); + let output: Rc<RefCell<dyn OutputInterface>> = + Rc::new(RefCell::new(BufferedOutput::new(None, false, None))); + let console_io = ConsoleIO::new(input.clone(), output, QuestionHelper::default()); + + input.borrow_mut().set_interactive(true); + assert!(console_io.is_interactive()); + input.borrow_mut().set_interactive(false); + assert!(!console_io.is_interactive()); } -#[ignore = "requires a PHPUnit mock of OutputInterface with expects()->method('write')->with() expectation verification; no mocking framework"] #[test] fn test_write() { - todo!() + // PHP: expects write('some information about something', false) at VERBOSITY_NORMAL. + // A plain (non-Console) OutputInterface routes write to the main output, so the message lands + // in the BufferedOutput verbatim (no trailing EOL since newline is false). + let input: Rc<RefCell<dyn InputInterface>> = + Rc::new(RefCell::new(ArrayInput::new(vec![], None).unwrap())); + let buffered = Rc::new(RefCell::new(BufferedOutput::new(None, false, None))); + let output: Rc<RefCell<dyn OutputInterface>> = buffered.clone(); + let console_io = ConsoleIO::new(input, output, QuestionHelper::default()); + + console_io.write3("some information about something", false, NORMAL); + + assert_eq!( + "some information about something", + buffered.borrow().fetch() + ); } -#[ignore = "requires a PHPUnit mock of ConsoleOutputInterface with getErrorOutput/write expectation verification; no mocking framework"] +#[ignore = "PHP mocks ConsoleOutputInterface so getErrorOutput returns the same mock; a real ConsoleOutput's error StreamOutput writes to php://stderr, which cannot be read back, and the trait offers no seam to inject a BufferedOutput error sink"] #[test] fn test_write_error() { todo!() } -#[ignore = "requires a PHPUnit mock of OutputInterface with a write() callback expectation verification; no mocking framework"] +#[ignore = "ConsoleIO::write3 takes a single &str; the test feeds a 2-element array ['First line','Second lines'] and asserts a per-element regex on the debugging-prefixed messages array, which the &str signature cannot represent"] #[test] fn test_write_with_multiple_line_string_when_debugging() { todo!() } -#[ignore = "requires a PHPUnit mock of OutputInterface with willReturnCallback series expectation verification; no mocking framework"] #[test] fn test_overwrite() { - todo!() + // PHP asserts the exact series of write() calls overwrite produces (backspaces, padding, etc.). + // PHP's mock intercepts each raw write() argument, before the output formatter runs. A real + // BufferedOutput captures the *rendered* text, so the `<question>/<comment>/<info>` tags are + // stripped while the backspace/space runs are identical. The backspace counts still derive from + // `strlen(strip_tags(lastMessage))`, so they match PHP exactly. The series, in order, is: + // 'something (strlen = 23)' + "\n" (initial write, newline defaults true) + // "\x08" * 23 (clear previous, strip_tags len = 23) + // 'shorter (12)' (new message, strip_tags len = 12) + // " " * 11 (fill: 23 - 12) + // "\x08" * 11 (move cursor back) + // "\x08" * 12 (clear previous, len = 12) + // 'something longer than initial (34)' (new message) + // The final overwrite needs no fill and newline defaults true, appending a trailing "\n". + let input: Rc<RefCell<dyn InputInterface>> = + Rc::new(RefCell::new(ArrayInput::new(vec![], None).unwrap())); + let buffered = Rc::new(RefCell::new(BufferedOutput::new(None, false, None))); + let output: Rc<RefCell<dyn OutputInterface>> = buffered.clone(); + let console_io = ConsoleIO::new(input, output, QuestionHelper::default()); + + console_io.write3("something (<question>strlen = 23</question>)", true, NORMAL); + console_io.overwrite4("shorter (<comment>12</comment>)", false, None, NORMAL); + console_io.overwrite4( + "something longer than initial (<info>34</info>)", + true, + None, + NORMAL, + ); + + let bs = |n: usize| "\u{08}".repeat(n); + let expected = format!( + "{}\n{}{}{}{}{}{}\n", + "something (strlen = 23)", + bs(23), + "shorter (12)", + " ".repeat(11), + bs(11), + bs(12), + "something longer than initial (34)", + ); + assert_eq!(expected, buffered.borrow().fetch()); } -#[ignore = "requires a PHPUnit mock of QuestionHelper/HelperSet with ask/get expectation verification; no mocking framework"] #[test] fn test_ask() { - todo!() + // PHP asserts QuestionHelper::ask receives a Question. Behaviorally, an interactive input whose + // stream yields the answer makes ConsoleIO::ask return that answer. + let (console_io, _output) = make_console_io_with_answer("answer\n"); + let result = console_io.ask("Why?".to_string(), PhpMixed::String("default".to_string())); + assert_eq!(PhpMixed::String("answer".to_string()), result); } -#[ignore = "requires a PHPUnit mock of QuestionHelper/HelperSet with ask/get expectation verification; no mocking framework"] #[test] fn test_ask_confirmation() { - todo!() + // PHP asserts the helper receives a StrictConfirmationQuestion. Behaviorally, a "yes" answer + // confirms and a "no" answer denies via the StrictConfirmationQuestion ConsoleIO builds. + let (console_io, _output) = make_console_io_with_answer("yes\n"); + assert!(console_io.ask_confirmation("Why?".to_string(), false)); + + let (console_io, _output) = make_console_io_with_answer("no\n"); + assert!(!console_io.ask_confirmation("Why?".to_string(), false)); } -#[ignore = "requires a PHPUnit mock of QuestionHelper/HelperSet with ask/get expectation verification; no mocking framework"] #[test] fn test_ask_and_validate() { - todo!() + // PHP asserts the helper receives a Question with a validator. Behaviorally, an always-true + // validator passes the answer straight through. + let (console_io, _output) = make_console_io_with_answer("answer\n"); + let validator: Box<dyn Fn(PhpMixed) -> anyhow::Result<PhpMixed>> = Box::new(|value| Ok(value)); + let result = console_io + .ask_and_validate( + "Why?".to_string(), + validator, + Some(10), + PhpMixed::String("default".to_string()), + ) + .unwrap(); + assert_eq!(PhpMixed::String("answer".to_string()), result); } -#[ignore = "requires a PHPUnit mock of QuestionHelper/HelperSet with ask/get expectation verification; no mocking framework"] #[test] fn test_select() { - todo!() + // PHP mocks the helper to return ['item2'] and asserts select maps it to ['1']. Behaviorally, + // feeding "item2" to a multiselect ChoiceQuestion over the non-associative list + // ["item1","item2"] resolves to that choice, which select maps back to its index '1'. + let (console_io, _output) = make_console_io_with_answer("item2\n"); + let result = console_io.select( + "Select item".to_string(), + vec!["item1".to_string(), "item2".to_string()], + PhpMixed::String("item1".to_string()), + PhpMixed::Bool(false), + "Error message".to_string(), + true, + ); + assert_eq!( + PhpMixed::List(vec![PhpMixed::String("1".to_string())]), + result + ); } #[test] diff --git a/crates/shirabe/tests/repository/vcs/git_bitbucket_driver_test.rs b/crates/shirabe/tests/repository/vcs/git_bitbucket_driver_test.rs index 2fedba1..944ef9a 100644 --- a/crates/shirabe/tests/repository/vcs/git_bitbucket_driver_test.rs +++ b/crates/shirabe/tests/repository/vcs/git_bitbucket_driver_test.rs @@ -9,21 +9,20 @@ use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::repository::vcs::GitBitbucketDriver; use shirabe::util::filesystem::Filesystem; -use shirabe_php_shim::PhpMixed; +use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler}; +use shirabe::util::process_executor::ProcessExecutor; +use shirabe_php_shim::{InvalidArgumentException, PhpMixed, RuntimeException}; use tempfile::TempDir; +use crate::http_downloader_mock::{HttpDownloaderMockGuard, expect_full, get_http_downloader_mock}; +use crate::io_stub::IOStub; + struct SetUp { home: TempDir, config: Config, - // The IOInterface mock and the HttpDownloaderMock are not ported. - io: (), - http_downloader: (), } fn set_up() -> SetUp { - // The IOInterface mock is created via PHPUnit's getMockBuilder, which is not ported. - let io: () = todo!(); - let home = TempDir::new().unwrap(); let mut config = Config::new(true, None); let mut top: IndexMap<String, PhpMixed> = IndexMap::new(); @@ -35,15 +34,7 @@ fn set_up() -> SetUp { top.insert("config".to_string(), PhpMixed::Array(config_section)); config.merge(&top, Config::SOURCE_UNKNOWN); - // The HttpDownloaderMock is created via getHttpDownloaderMock, which is not ported. - let http_downloader: () = todo!(); - - SetUp { - home, - config, - io, - http_downloader, - } + SetUp { home, config } } fn tear_down(home: &std::path::Path) { @@ -67,6 +58,256 @@ impl Drop for TearDown { } } +// 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 getDriver: constructs the driver from the given repo url with a mocked +// IOInterface, the test config, the mocked HttpDownloader, and a real ProcessExecutor, +// then calls initialize(). +fn get_driver( + url: &str, + io: Rc<RefCell<dyn IOInterface>>, + config: Rc<RefCell<Config>>, + http_downloader: Rc<RefCell<HttpDownloader>>, +) -> anyhow::Result<GitBitbucketDriver> { + let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_config.insert("url".to_string(), PhpMixed::String(url.to_string())); + + let process = Rc::new(RefCell::new(ProcessExecutor::new(Some(io.clone())))); + + let mut driver = GitBitbucketDriver::new(repo_config, io, config, http_downloader, process); + driver.initialize()?; + Ok(driver) +} + +#[test] +fn test_get_root_identifier_wrong_scm_type() { + 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): (_, HttpDownloaderMockGuard) = get_http_downloader_mock( + vec![http_body( + "https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner", + r#"{"scm":"hg","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo","name":"https"},{"href":"ssh:\/\/hg@bitbucket.org\/user\/repo","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}"#, + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let mut driver = get_driver( + "https://bitbucket.org/user/repo.git", + io, + config, + http_downloader, + ) + .unwrap(); + + let err = driver.get_root_identifier().unwrap_err(); + let runtime = err + .downcast_ref::<RuntimeException>() + .expect("expected RuntimeException"); + assert_eq!( + "https://bitbucket.org/user/repo.git does not appear to be a git repository, use https://bitbucket.org/user/repo but remember that Bitbucket no longer supports the mercurial repositories. https://bitbucket.org/blog/sunsetting-mercurial-support-in-bitbucket", + runtime.message + ); +} + +#[test] +#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"] +fn test_driver() { + 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 urls = [ + "https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner", + "https://api.bitbucket.org/2.0/repositories/user/repo/refs/tags?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cnext&sort=-target.date", + "https://api.bitbucket.org/2.0/repositories/user/repo/refs/branches?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cvalues.heads%2Cnext&sort=-target.date", + "https://api.bitbucket.org/2.0/repositories/user/repo/src/main/composer.json", + "https://api.bitbucket.org/2.0/repositories/user/repo/commit/main?fields=date", + ]; + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + http_body( + urls[0], + r#"{"mainbranch": {"name": "main"}, "scm":"git","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo.git","name":"https"},{"href":"ssh:\/\/git@bitbucket.org\/user\/repo.git","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}"#, + ), + http_body( + urls[1], + r#"{"values":[{"name":"1.0.1","target":{"hash":"9b78a3932143497c519e49b8241083838c8ff8a1"}},{"name":"1.0.0","target":{"hash":"d3393d514318a9267d2f8ebbf463a9aaa389f8eb"}}]}"#, + ), + http_body( + urls[2], + r#"{"values":[{"name":"main","target":{"hash":"937992d19d72b5116c3e8c4a04f960e5fa270b22"}}]}"#, + ), + http_body( + urls[3], + r#"{"name": "user/repo","description": "test repo","license": "GPL","authors": [{"name": "Name","email": "local@domain.tld"}],"require": {"creator/package": "^1.0"},"require-dev": {"phpunit/phpunit": "~4.8"}}"#, + ), + http_body(urls[4], r#"{"date": "2016-05-17T13:19:52+00:00"}"#), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let mut driver = get_driver( + "https://bitbucket.org/user/repo.git", + io, + config, + http_downloader, + ) + .unwrap(); + + assert_eq!("main", driver.get_root_identifier().unwrap()); + + let mut expected_tags: IndexMap<String, String> = IndexMap::new(); + expected_tags.insert( + "1.0.1".to_string(), + "9b78a3932143497c519e49b8241083838c8ff8a1".to_string(), + ); + expected_tags.insert( + "1.0.0".to_string(), + "d3393d514318a9267d2f8ebbf463a9aaa389f8eb".to_string(), + ); + assert_eq!(expected_tags, driver.get_tags().unwrap()); + + let mut expected_branches: IndexMap<String, String> = IndexMap::new(); + expected_branches.insert( + "main".to_string(), + "937992d19d72b5116c3e8c4a04f960e5fa270b22".to_string(), + ); + assert_eq!(expected_branches, driver.get_branches().unwrap()); + + let data = driver.get_composer_information("main").unwrap().unwrap(); + assert_eq!( + Some("user/repo"), + data.get("name").and_then(|v| v.as_string()) + ); + assert_eq!( + Some("test repo"), + data.get("description").and_then(|v| v.as_string()) + ); + assert_eq!( + Some("2016-05-17T13:19:52+00:00"), + data.get("time").and_then(|v| v.as_string()) + ); + let support = data.get("support").and_then(|v| v.as_array()).unwrap(); + assert_eq!( + Some( + "https://bitbucket.org/user/repo/src/937992d19d72b5116c3e8c4a04f960e5fa270b22/?at=main" + ), + support.get("source").and_then(|v| v.as_string()) + ); + assert_eq!( + Some("https://bitbucket.org/user/repo"), + data.get("homepage").and_then(|v| v.as_string()) + ); + + // testGetParams (PHP @depends testDriver): assertions over the same driver. + let url = "https://bitbucket.org/user/repo.git"; + assert_eq!(url, driver.get_url()); + + let dist = driver.get_dist("reference").unwrap(); + assert_eq!(Some("zip"), dist.get("type").map(String::as_str)); + assert_eq!( + Some("https://bitbucket.org/user/repo/get/reference.zip"), + dist.get("url").map(String::as_str) + ); + assert_eq!(Some("reference"), dist.get("reference").map(String::as_str)); + assert_eq!(Some(""), dist.get("shasum").map(String::as_str)); + + let source = driver.get_source("reference"); + assert_eq!(Some("git"), source.get("type").map(String::as_str)); + assert_eq!(Some(url), source.get("url").map(String::as_str)); + assert_eq!( + Some("reference"), + source.get("reference").map(String::as_str) + ); +} + +#[test] +fn test_initialize_invalid_repository_url() { + 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 result = get_driver("https://bitbucket.org/acme", io, config, http_downloader); + let err = result.unwrap_err(); + assert!( + err.downcast_ref::<InvalidArgumentException>().is_some(), + "expected InvalidArgumentException, got: {err:?}" + ); +} + +#[test] +#[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 _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 repo_url = "https://bitbucket.org/user/repo.git"; + + let urls = [ + "https://api.bitbucket.org/2.0/repositories/user/repo?fields=-project%2C-owner", + "https://api.bitbucket.org/2.0/repositories/user/repo/src/main/composer.json", + "https://api.bitbucket.org/2.0/repositories/user/repo/commit/main?fields=date", + "https://api.bitbucket.org/2.0/repositories/user/repo/refs/tags?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cnext&sort=-target.date", + "https://api.bitbucket.org/2.0/repositories/user/repo/refs/branches?pagelen=100&fields=values.name%2Cvalues.target.hash%2Cvalues.heads%2Cnext&sort=-target.date", + ]; + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + http_body( + urls[0], + r#"{"mainbranch": {"name": "main"}, "scm":"git","website":"","has_wiki":false,"name":"repo","links":{"branches":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/branches"},"tags":{"href":"https:\/\/api.bitbucket.org\/2.0\/repositories\/user\/repo\/refs\/tags"},"clone":[{"href":"https:\/\/user@bitbucket.org\/user\/repo.git","name":"https"},{"href":"ssh:\/\/git@bitbucket.org\/user\/repo.git","name":"ssh"}],"html":{"href":"https:\/\/bitbucket.org\/user\/repo"}},"language":"php","created_on":"2015-02-18T16:22:24.688+00:00","updated_on":"2016-05-17T13:20:21.993+00:00","is_private":true,"has_issues":false}"#, + ), + http_body(urls[1], format!(r#"{{"support": "{repo_url}"}}"#)), + http_body(urls[2], r#"{"date": "2016-05-17T13:19:52+00:00"}"#), + http_body( + urls[3], + r#"{"values":[{"name":"1.0.1","target":{"hash":"9b78a3932143497c519e49b8241083838c8ff8a1"}},{"name":"1.0.0","target":{"hash":"d3393d514318a9267d2f8ebbf463a9aaa389f8eb"}}]}"#, + ), + http_body( + urls[4], + r#"{"values":[{"name":"main","target":{"hash":"937992d19d72b5116c3e8c4a04f960e5fa270b22"}}]}"#, + ), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let mut driver = get_driver(repo_url, io, config, http_downloader).unwrap(); + + driver.get_root_identifier().unwrap(); + let data = driver.get_composer_information("main").unwrap().unwrap(); + + let support = data.get("support").and_then(|v| v.as_array()).unwrap(); + assert_eq!( + Some( + "https://bitbucket.org/user/repo/src/937992d19d72b5116c3e8c4a04f960e5fa270b22/?at=main" + ), + support.get("source").and_then(|v| v.as_string()) + ); +} + #[test] fn test_supports() { let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); @@ -98,71 +339,3 @@ fn test_supports() { .unwrap() ); } - -// The remaining cases construct a GitBitbucketDriver and mock the HttpDownloader to return -// Bitbucket API responses; mocking is not available, and a real HttpDownloader reaches -// curl_multi_init (todo!()). -#[test] -#[ignore = "needs IOInterface mock (getMockBuilder) and getHttpDownloaderMock/HttpDownloaderMock (not ported); set_up()'s io and http_downloader are todo!() and real HttpDownloader hits todo!() curl I/O"] -fn test_get_root_identifier_wrong_scm_type() { - let SetUp { - home, - config: _, - io: _, - http_downloader: _, - } = set_up(); - let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() -} - -#[test] -#[ignore = "needs IOInterface mock (getMockBuilder) and getHttpDownloaderMock/HttpDownloaderMock (not ported); set_up()'s io and http_downloader are todo!() and real HttpDownloader hits todo!() curl I/O"] -fn test_driver() { - let SetUp { - home, - config: _, - io: _, - http_downloader: _, - } = set_up(); - let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() -} - -#[test] -#[ignore = "needs IOInterface mock (getMockBuilder) and getHttpDownloaderMock/HttpDownloaderMock (not ported); depends on test_driver's driver and set_up()'s todo!() mocks"] -fn test_get_params() { - let SetUp { - home, - config: _, - io: _, - http_downloader: _, - } = set_up(); - let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() -} - -#[test] -#[ignore = "needs IOInterface mock (getMockBuilder) and getHttpDownloaderMock/HttpDownloaderMock (not ported); set_up()'s io and http_downloader are todo!()"] -fn test_initialize_invalid_repository_url() { - let SetUp { - home, - config: _, - io: _, - http_downloader: _, - } = set_up(); - let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() -} - -#[test] -#[ignore = "needs IOInterface mock (getMockBuilder) and getHttpDownloaderMock/HttpDownloaderMock (not ported); set_up()'s io and http_downloader are todo!() and real HttpDownloader hits todo!() curl I/O"] -fn test_invalid_support_data() { - let SetUp { - home, - config: _, - io: _, - http_downloader: _, - } = set_up(); - let _tear_down = TearDown::new(home.path().to_path_buf()); - todo!() -} diff --git a/crates/shirabe/tests/util/auth_helper_test.rs b/crates/shirabe/tests/util/auth_helper_test.rs index 21bf1c6..133fd75 100644 --- a/crates/shirabe/tests/util/auth_helper_test.rs +++ b/crates/shirabe/tests/util/auth_helper_test.rs @@ -1,130 +1,611 @@ //! ref: composer/tests/Composer/Test/Util/AuthHelperTest.php -// These mock IO/Config to drive AuthHelper's header/option building and interactive auth -// storage; mocking is not available here. +use std::cell::RefCell; +use std::rc::Rc; -#[allow(dead_code)] -fn set_up() { - // Builds mocked IOInterface/Config and a real AuthHelper; mocking is not available. - todo!() +use indexmap::IndexMap; +use shirabe::config::ConfigSourceInterface; +use shirabe::io::IOInterface; +use shirabe::io::io_interface; +use shirabe::util::{AuthHelper, Bitbucket, StoreAuth}; +use shirabe_php_shim::{PhpMixed, base64_encode, json_encode}; + +use crate::config_stub::ConfigStubBuilder; +use crate::io_mock::{Expectation, IOMock, get_io_mock}; + +// Mirrors AuthHelperTest::setUp: a DEBUG-verbosity IOMock plus a real Config, both +// shared with the AuthHelper under test. The IOMockGuard runs assert_complete on drop. +struct Fixture { + io: Rc<RefCell<IOMock>>, + config: Rc<RefCell<shirabe::config::Config>>, + auth_helper: AuthHelper, + _guard: crate::io_mock::IOMockGuard, +} + +fn set_up_with_config(config: Rc<RefCell<shirabe::config::Config>>) -> Fixture { + let (mock, guard) = get_io_mock(io_interface::DEBUG).unwrap(); + let io: Rc<RefCell<dyn IOInterface>> = mock.clone(); + let auth_helper = AuthHelper::new(io, config.clone()); + Fixture { + io: mock, + config, + auth_helper, + _guard: guard, + } +} + +fn set_up() -> Fixture { + set_up_with_config(ConfigStubBuilder::new().build_shared()) +} + +// Mirrors AuthHelperTest::expectsAuthentication: pre-seed the IO so hasAuthentication +// and getAuthentication return the given credentials for `origin`. +fn expects_authentication(io: &Rc<RefCell<IOMock>>, origin: &str, username: &str, password: &str) { + use shirabe::io::IOInterfaceMutable; + io.borrow_mut().set_authentication( + origin.to_string(), + username.to_string(), + Some(password.to_string()), + ); +} + +fn header_strings(options: &IndexMap<String, PhpMixed>) -> Vec<String> { + options + .get("http") + .and_then(|v| v.as_array()) + .and_then(|http| http.get("header")) + .and_then(|v| v.as_list()) + .map(|list| { + list.iter() + .map(|v| v.as_string().unwrap_or("").to_string()) + .collect() + }) + .unwrap_or_default() +} + +fn base_headers() -> Vec<PhpMixed> { + vec![ + PhpMixed::String("Accept-Encoding: gzip".to_string()), + PhpMixed::String("Connection: close".to_string()), + ] +} + +fn http_options_with_headers() -> IndexMap<String, PhpMixed> { + let mut http = IndexMap::new(); + http.insert("header".to_string(), PhpMixed::List(base_headers())); + let mut options = IndexMap::new(); + options.insert("http".to_string(), PhpMixed::Array(http)); + options } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] fn test_add_authentication_header_without_auth_credentials() { - todo!() + let mut f = set_up(); + let options = http_options_with_headers(); + let origin = "http://example.org"; + let url = "file://example"; + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec!["Accept-Encoding: gzip", "Connection: close"] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] fn test_add_authentication_header_with_bearer_password() { - todo!() + let mut f = set_up(); + let options = http_options_with_headers(); + let origin = "http://example.org"; + let url = "file://example"; + expects_authentication(&f.io, origin, "my_username", "bearer"); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec![ + "Accept-Encoding: gzip", + "Connection: close", + "Authorization: Bearer my_username", + ] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] fn test_add_authentication_header_with_github_token() { - todo!() + let mut f = set_up(); + f.io.borrow_mut() + .expects( + vec![Expectation::text("Using GitHub token authentication")], + false, + ) + .unwrap(); + let options = http_options_with_headers(); + let origin = "github.com"; + let url = "https://api.github.com/"; + expects_authentication(&f.io, origin, "my_username", "x-oauth-basic"); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec![ + "Accept-Encoding: gzip", + "Connection: close", + "Authorization: token my_username", + ] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config with expects()/willReturn(); no mocking framework"] fn test_add_authentication_header_with_gitlab_oath_token() { - todo!() + let config = ConfigStubBuilder::new() + .with( + "gitlab-domains", + PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]), + ) + .build_shared(); + let mut f = set_up_with_config(config); + f.io.borrow_mut() + .expects( + vec![Expectation::text("Using GitLab OAuth token authentication")], + false, + ) + .unwrap(); + let options = http_options_with_headers(); + let origin = "gitlab.com"; + let url = "https://api.gitlab.com/"; + expects_authentication(&f.io, origin, "my_username", "oauth2"); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec![ + "Accept-Encoding: gzip", + "Connection: close", + "Authorization: Bearer my_username", + ] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] fn test_add_authentication_options_for_client_certificate() { - todo!() + let mut f = set_up(); + let options = IndexMap::new(); + let origin = "example.org"; + let url = "file://example"; + + let mut certificate_configuration = IndexMap::new(); + certificate_configuration.insert( + "local_cert".to_string(), + PhpMixed::String("certificate value".to_string()), + ); + certificate_configuration.insert( + "local_pk".to_string(), + PhpMixed::String("key value".to_string()), + ); + certificate_configuration.insert( + "passphrase".to_string(), + PhpMixed::String("passphrase value".to_string()), + ); + let password = json_encode(&PhpMixed::Array(certificate_configuration.clone())).unwrap(); + expects_authentication(&f.io, origin, "client-certificate", &password); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + options.get("ssl"), + Some(&PhpMixed::Array(certificate_configuration)) + ); +} + +fn add_authentication_header_with_gitlab_private_token(password: &str) { + let config = ConfigStubBuilder::new() + .with( + "gitlab-domains", + PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]), + ) + .build_shared(); + let mut f = set_up_with_config(config); + f.io.borrow_mut() + .expects( + vec![Expectation::text( + "Using GitLab private token authentication", + )], + false, + ) + .unwrap(); + let options = http_options_with_headers(); + let origin = "gitlab.com"; + let url = "https://api.gitlab.com/"; + expects_authentication(&f.io, origin, "my_username", password); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec![ + "Accept-Encoding: gzip", + "Connection: close", + "PRIVATE-TOKEN: my_username", + ] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config with expects()/willReturn(); no mocking framework"] fn test_add_authentication_header_with_gitlab_private_token() { - todo!() + add_authentication_header_with_gitlab_private_token("private-token"); + add_authentication_header_with_gitlab_private_token("gitlab-ci-token"); } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] fn test_add_authentication_header_with_bitbucket_oath_token() { - todo!() + let mut f = set_up(); + f.io.borrow_mut() + .expects( + vec![Expectation::text( + "Using Bitbucket OAuth token authentication", + )], + false, + ) + .unwrap(); + let options = http_options_with_headers(); + let origin = "bitbucket.org"; + let url = "https://bitbucket.org/site/oauth2/authorize"; + expects_authentication(&f.io, origin, "x-token-auth", "my_password"); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec![ + "Accept-Encoding: gzip", + "Connection: close", + "Authorization: Bearer my_password", + ] + ); +} + +fn add_authentication_header_with_bitbucket_public_url(url: &str) { + let mut f = set_up(); + let options = http_options_with_headers(); + let origin = "bitbucket.org"; + expects_authentication(&f.io, origin, "x-token-auth", "my_password"); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec!["Accept-Encoding: gzip", "Connection: close"] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] fn test_add_authentication_header_with_bitbucket_public_url() { - todo!() + add_authentication_header_with_bitbucket_public_url( + "https://bitbucket.org/user/repo/downloads/whatever", + ); + add_authentication_header_with_bitbucket_public_url( + "https://bbuseruploads.s3.amazonaws.com/9421ee72-638e-43a9-82ea-39cfaae2bfaa/downloads/b87c59d9-54f3-4922-b711-d89059ec3bcf", + ); } -#[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] -fn test_add_authentication_header_with_basic_http_authentication() { - todo!() +fn add_authentication_header_with_basic_http_authentication( + url: &str, + origin: &str, + username: &str, + password: &str, +) { + let mut f = set_up(); + f.io.borrow_mut() + .expects( + vec![Expectation::text(format!( + "Using HTTP basic authentication with username \"{}\"", + username + ))], + false, + ) + .unwrap(); + let options = http_options_with_headers(); + expects_authentication(&f.io, origin, username, password); + + let expected = format!( + "Authorization: Basic {}", + base64_encode(&format!("{}:{}", username, password)) + ); + + let options = f + .auth_helper + .add_authentication_options(options, origin, url) + .unwrap(); + + assert_eq!( + header_strings(&options), + vec![ + "Accept-Encoding: gzip".to_string(), + "Connection: close".to_string(), + expected, + ] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn(); no mocking framework"] -fn test_add_authentication_header_with_custom_headers() { - todo!() +fn test_add_authentication_header_with_basic_http_authentication() { + add_authentication_header_with_basic_http_authentication( + Bitbucket::OAUTH2_ACCESS_TOKEN_URL, + "bitbucket.org", + "x-token-auth", + "my_password", + ); + add_authentication_header_with_basic_http_authentication( + "https://some-api.url.com", + "some-api.url.com", + "my_username", + "my_password", + ); + add_authentication_header_with_basic_http_authentication( + "https://gitlab.com", + "gitlab.com", + "my_username", + "my_password", + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config in setUp; no mocking framework"] fn test_is_public_bit_bucket_download_with_bitbucket_public_url() { - todo!() + let f = set_up(); + assert!( + f.auth_helper + .is_public_bit_bucket_download("https://bitbucket.org/user/repo/downloads/whatever") + ); + assert!(f.auth_helper.is_public_bit_bucket_download( + "https://bbuseruploads.s3.amazonaws.com/9421ee72-638e-43a9-82ea-39cfaae2bfaa/downloads/b87c59d9-54f3-4922-b711-d89059ec3bcf", + )); } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config in setUp; no mocking framework"] fn test_is_public_bit_bucket_download_with_non_bitbucket_public_url() { - todo!() + let f = set_up(); + assert!( + !f.auth_helper + .is_public_bit_bucket_download("https://bitbucket.org/site/oauth2/authorize") + ); +} + +// Records addConfigSetting calls and serves a configurable getName, mirroring the +// PHPUnit mock of ConfigSourceInterface used by the storeAuth tests. +#[derive(Debug)] +struct ConfigSourceMock { + name: String, + added: Rc<RefCell<Vec<(String, PhpMixed)>>>, +} + +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<()> { + self.added.borrow_mut().push((name.to_string(), value)); + Ok(()) + } + fn remove_config_setting(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + 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 expected_auth_setting(username: &str, password: &str) -> PhpMixed { + let mut auth = IndexMap::new(); + auth.insert( + "username".to_string(), + PhpMixed::String(username.to_string()), + ); + auth.insert( + "password".to_string(), + PhpMixed::String(password.to_string()), + ); + PhpMixed::Array(auth) } #[test] -#[ignore = "requires PHPUnit mock Config/ConfigSourceInterface with expects()/willReturn(); no mocking framework"] fn test_store_auth_automatically() { - todo!() + let mut f = set_up(); + let origin = "github.com"; + expects_authentication(&f.io, origin, "my_username", "my_password"); + + let added = Rc::new(RefCell::new(Vec::new())); + f.config + .borrow_mut() + .set_auth_config_source(Box::new(ConfigSourceMock { + name: "https://api.gitlab.com/source".to_string(), + added: added.clone(), + })); + + f.auth_helper + .store_auth(origin, StoreAuth::Bool(true)) + .unwrap(); + + assert_eq!( + *added.borrow(), + vec![( + "http-basic.github.com".to_string(), + expected_auth_setting("my_username", "my_password"), + )] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config/ConfigSourceInterface with willReturnCallback(); no mocking framework"] fn test_store_auth_with_prompt_yes_answer() { - todo!() + let mut f = set_up(); + let origin = "github.com"; + expects_authentication(&f.io, origin, "my_username", "my_password"); + let config_source_name = "https://api.gitlab.com/source"; + + f.io.borrow_mut() + .expects( + vec![Expectation::ask( + format!( + "Do you want to store credentials for {} in {} ? [Yn] ", + origin, config_source_name + ), + "y", + )], + false, + ) + .unwrap(); + + let added = Rc::new(RefCell::new(Vec::new())); + f.config + .borrow_mut() + .set_auth_config_source(Box::new(ConfigSourceMock { + name: config_source_name.to_string(), + added: added.clone(), + })); + + f.auth_helper.store_auth(origin, StoreAuth::Prompt).unwrap(); + + assert_eq!( + *added.borrow(), + vec![( + "http-basic.github.com".to_string(), + expected_auth_setting("my_username", "my_password"), + )] + ); } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config/ConfigSourceInterface with willReturnCallback(); no mocking framework"] fn test_store_auth_with_prompt_no_answer() { - todo!() + let mut f = set_up(); + let origin = "github.com"; + let config_source_name = "https://api.gitlab.com/source"; + + f.io.borrow_mut() + .expects( + vec![Expectation::ask( + format!( + "Do you want to store credentials for {} in {} ? [Yn] ", + origin, config_source_name + ), + "n", + )], + false, + ) + .unwrap(); + + let added = Rc::new(RefCell::new(Vec::new())); + f.config + .borrow_mut() + .set_auth_config_source(Box::new(ConfigSourceMock { + name: config_source_name.to_string(), + added: added.clone(), + })); + + f.auth_helper.store_auth(origin, StoreAuth::Prompt).unwrap(); + + assert!(added.borrow().is_empty()); } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config/ConfigSourceInterface with willReturnCallback(); no mocking framework"] +#[ignore = "PHP catches a RuntimeException from the validator; the Rust QuestionHelper exhausts \ +input and panics via expect() rather than returning Err, so it cannot be represented as a \ +recoverable Result"] fn test_store_auth_with_prompt_invalid_answer() { todo!() } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config with expects()/willReturnMap(); no mocking framework"] +#[ignore = "needs the extra prompt_auth_if_needed params (headers/retry_count/response_body) and \ +GitLab::authorize_oauth which shells out to real `git config`; depends on host git state and is a \ +design-level port concern"] fn test_prompt_auth_if_needed_git_lab_no_auth_change() { todo!() } #[test] -#[ignore = "requires PHPUnit mock IOInterface/Config with willReturnMap()/willReturnCallback(); no mocking framework"] +#[ignore = "drives Bitbucket::request_token over the network and relies on willReturnCallback \ +sequencing of getAuthentication; design-level port concern"] fn test_prompt_auth_if_needed_multiple_bitbucket_downloads() { todo!() } #[test] -#[ignore = "requires PHPUnit mock IOInterface with expects()/willReturn() plus set_error_handler; no mocking framework"] +#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \ +trigger_error/E_USER_DEPRECATED; the PHP error-handler subsystem is not modeled"] +fn test_add_authentication_header_with_custom_headers() { + todo!() +} + +#[test] +#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \ +trigger_error/E_USER_DEPRECATED; the PHP error-handler subsystem is not modeled"] fn test_add_authentication_header_is_working() { todo!() } #[test] -#[ignore = "requires PHPUnit mock IOInterface in setUp plus set_error_handler deprecation capture; no mocking framework"] +#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \ +trigger_error/E_USER_DEPRECATED converted to a RuntimeException via set_error_handler; not modeled"] fn test_add_authentication_header_deprecation() { todo!() } diff --git a/crates/shirabe/tests/util/main.rs b/crates/shirabe/tests/util/main.rs index d72bcc0..0208ca4 100644 --- a/crates/shirabe/tests/util/main.rs +++ b/crates/shirabe/tests/util/main.rs @@ -2,6 +2,8 @@ mod config_stub; #[path = "../common/http_downloader_mock.rs"] mod http_downloader_mock; +#[path = "../common/io_mock.rs"] +mod io_mock; #[path = "../common/io_stub.rs"] mod io_stub; #[path = "../common/process_executor_mock.rs"] |
