aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/repository
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 15:24:55 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commitd4cdccb8de8758bd46a12283f8df90e020327b99 (patch)
treecd7c928cff2615a0bd599cf1f9e09c9852c399d7 /crates/shirabe/tests/repository
parent8117e5450693d19726da877bce8aacf5c7aa53af (diff)
downloadphp-shirabe-d4cdccb8de8758bd46a12283f8df90e020327b99.tar.gz
php-shirabe-d4cdccb8de8758bd46a12283f8df90e020327b99.tar.zst
php-shirabe-d4cdccb8de8758bd46a12283f8df90e020327b99.zip
test: port 70 perforce/repository/downloader/installer/dispatcher tests
Port perforce (36), locker (10), composer_repository (7), installation_manager (6), file_downloader (5), and event_dispatcher (6) tests via the mock infra. Fix production porting bugs surfaced en route: BufferIO::get_output look-behind regex, ComposerRepository list-form package iteration and initialize dispatch, gethostname and spl_autoload_functions shims; add EventDispatcher get_listeners test seam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/repository')
-rw-r--r--crates/shirabe/tests/repository/composer_repository_test.rs749
1 files changed, 728 insertions, 21 deletions
diff --git a/crates/shirabe/tests/repository/composer_repository_test.rs b/crates/shirabe/tests/repository/composer_repository_test.rs
index d36c2e1..1cb34ed 100644
--- a/crates/shirabe/tests/repository/composer_repository_test.rs
+++ b/crates/shirabe/tests/repository/composer_repository_test.rs
@@ -1,59 +1,766 @@
//! ref: composer/tests/Composer/Test/Repository/ComposerRepositoryTest.php
-// These construct a ComposerRepository with a mocked HttpDownloader/IO/Config and parse
-// provider/package data whose constraints go through a look-around regex; mocking is not
-// available and a real HttpDownloader reaches curl_multi_init (todo!()).
-#[ignore = "needs PHPUnit getMockBuilder to override loadRootServerFile; no method-mocking framework ported"]
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use indexmap::IndexMap;
+use shirabe::config::Config;
+use shirabe::io::IOInterface;
+use shirabe::io::null_io::NullIO;
+use shirabe::repository::RepositoryInterface;
+use shirabe::repository::SEARCH_FULLTEXT;
+use shirabe::repository::composer_repository::{ComposerRepository, ProviderListingEntry};
+use shirabe::util::http_downloader::HttpDownloaderMockHandler;
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::constraint::{AnyConstraint, SimpleConstraint};
+use tempfile::TempDir;
+
+use crate::http_downloader_mock::{HttpDownloaderMockGuard, expect_full, get_http_downloader_mock};
+
+// Mirrors PHP's `['url' => .., 'body' => ..]` mock entry (status defaults to 200,
+// options match any executed options).
+fn http_body(
+ url: &str,
+ body: impl Into<String>,
+) -> shirabe::util::http_downloader::HttpDownloaderMockExpectation {
+ expect_full(url, None, 200, body, vec![String::new()])
+}
+
+// Equivalent to FactoryMock::createConfig(): a real Config with a writable, unique
+// home directory. The TempDir is returned so it outlives the test.
+fn create_config() -> (Config, TempDir) {
+ let home = TempDir::new().unwrap();
+ let mut config = Config::new(true, None);
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert(
+ "home".to_string(),
+ PhpMixed::String(home.path().to_string_lossy().into_owned()),
+ );
+ top.insert("config".to_string(), PhpMixed::Array(config_section));
+ let mut repositories: IndexMap<String, PhpMixed> = IndexMap::new();
+ repositories.insert("packagist".to_string(), PhpMixed::Bool(false));
+ top.insert("repositories".to_string(), PhpMixed::Array(repositories));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+ (config, home)
+}
+
+fn create_config_read_only() -> (Config, TempDir) {
+ let (mut config, home) = create_config();
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert("cache-read-only".to_string(), PhpMixed::Bool(true));
+ top.insert("config".to_string(), PhpMixed::Array(config_section));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+ (config, home)
+}
+
+fn null_io() -> Rc<RefCell<dyn IOInterface>> {
+ Rc::new(RefCell::new(NullIO::new()))
+}
+
+fn repo_config(url: &str) -> IndexMap<String, PhpMixed> {
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ repo_config
+}
+
+fn json_encode(value: &PhpMixed) -> String {
+ shirabe::json::json_file::JsonFile::encode(value)
+}
+
+fn str_kv(pairs: &[(&str, PhpMixed)]) -> PhpMixed {
+ let mut m: IndexMap<String, PhpMixed> = IndexMap::new();
+ for (k, v) in pairs {
+ m.insert(k.to_string(), v.clone());
+ }
+ PhpMixed::Array(m)
+}
+
+// loadDataProvider cases: (expected name/version pairs, repoPackages body).
+fn load_data_provider() -> Vec<(Vec<(&'static str, &'static str)>, PhpMixed)> {
+ vec![
+ // Old repository format
+ (
+ vec![("foo/bar", "1.0.0")],
+ str_kv(&[(
+ "foo/bar",
+ str_kv(&[
+ ("name", PhpMixed::String("foo/bar".to_string())),
+ (
+ "versions",
+ str_kv(&[(
+ "1.0.0",
+ str_kv(&[
+ ("name", PhpMixed::String("foo/bar".to_string())),
+ ("version", PhpMixed::String("1.0.0".to_string())),
+ ]),
+ )]),
+ ),
+ ]),
+ )]),
+ ),
+ // New repository format
+ (
+ vec![("bar/foo", "3.14"), ("bar/foo", "3.145")],
+ str_kv(&[(
+ "packages",
+ str_kv(&[(
+ "bar/foo",
+ str_kv(&[
+ (
+ "3.14",
+ str_kv(&[
+ ("name", PhpMixed::String("bar/foo".to_string())),
+ ("version", PhpMixed::String("3.14".to_string())),
+ ]),
+ ),
+ (
+ "3.145",
+ str_kv(&[
+ ("name", PhpMixed::String("bar/foo".to_string())),
+ ("version", PhpMixed::String("3.145".to_string())),
+ ]),
+ ),
+ ]),
+ )]),
+ )]),
+ ),
+ // New repository format but without versions as keys should also be supported
+ (
+ vec![("bar/foo", "3.14"), ("bar/foo", "3.145")],
+ str_kv(&[(
+ "packages",
+ str_kv(&[(
+ "bar/foo",
+ PhpMixed::List(vec![
+ str_kv(&[
+ ("name", PhpMixed::String("bar/foo".to_string())),
+ ("version", PhpMixed::String("3.14".to_string())),
+ ]),
+ str_kv(&[
+ ("name", PhpMixed::String("bar/foo".to_string())),
+ ("version", PhpMixed::String("3.145".to_string())),
+ ]),
+ ]),
+ )]),
+ )]),
+ ),
+ ]
+}
+
#[test]
fn test_load_data() {
- todo!()
+ for (expected, repo_packages) in load_data_provider() {
+ let (config, _home) = create_config();
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![http_body(
+ "http://example.org/packages.json",
+ json_encode(&repo_packages),
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repository = ComposerRepository::new(
+ repo_config("http://example.org"),
+ null_io(),
+ &config,
+ http_downloader,
+ None,
+ )
+ .unwrap();
+
+ let packages = repository.get_packages().unwrap();
+
+ assert_eq!(expected.len(), packages.len());
+ for (index, (name, version)) in expected.iter().enumerate() {
+ assert_eq!(
+ format!("{} {}", name, version),
+ format!(
+ "{} {}",
+ packages[index].get_name(),
+ packages[index].get_pretty_version()
+ )
+ );
+ }
+ }
}
-#[ignore = "needs getMockBuilder to override fetchFile plus ReflectionProperty/ReflectionMethod to set private props and invoke whatProvides; no mocking/reflection ported"]
+// Ported and exercising the real whatProvides path, but blocked by an unimplemented
+// production method: building the dev-* alias packages reaches AliasPackage::get_source_type
+// (todo!() in package/alias_package.rs).
+#[ignore = "production todo!(): AliasPackage::get_source_type unimplemented (reached when building dev-* branch aliases)"]
#[test]
fn test_what_provides() {
- todo!()
+ let (config, _home) = create_config();
+
+ // The fetchFile response that PHP stubs via a method mock; here we serve it over
+ // HTTP and let the real fetchFile path parse it. fetchFile verifies the body's
+ // sha256 against the providerListing hash, so the listing hash is the actual hash
+ // of this body.
+ let body = json_encode(&str_kv(&[(
+ "packages",
+ PhpMixed::List(vec![
+ PhpMixed::List(vec![str_kv(&[
+ ("uid", PhpMixed::Int(1)),
+ ("name", PhpMixed::String("a".to_string())),
+ ("version", PhpMixed::String("dev-master".to_string())),
+ (
+ "extra",
+ str_kv(&[(
+ "branch-alias",
+ str_kv(&[("dev-master", PhpMixed::String("1.0.x-dev".to_string()))]),
+ )]),
+ ),
+ ])]),
+ PhpMixed::List(vec![str_kv(&[
+ ("uid", PhpMixed::Int(2)),
+ ("name", PhpMixed::String("a".to_string())),
+ ("version", PhpMixed::String("dev-develop".to_string())),
+ (
+ "extra",
+ str_kv(&[(
+ "branch-alias",
+ str_kv(&[("dev-develop", PhpMixed::String("1.1.x-dev".to_string()))]),
+ )]),
+ ),
+ ])]),
+ PhpMixed::List(vec![str_kv(&[
+ ("uid", PhpMixed::Int(3)),
+ ("name", PhpMixed::String("a".to_string())),
+ ("version", PhpMixed::String("0.6".to_string())),
+ ])]),
+ ]),
+ )]));
+
+ let sha256 = shirabe_php_shim::hash("sha256", &body);
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![http_body("https://dummy.test.link/to/a/file", body)],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let repo = ComposerRepository::new(
+ repo_config("https://dummy.test.link"),
+ null_io(),
+ &config,
+ http_downloader,
+ None,
+ )
+ .unwrap();
+
+ let rc = Rc::new(RefCell::new(repo));
+ let rc_dyn: Rc<RefCell<dyn RepositoryInterface>> = rc.clone();
+ rc.borrow().set_self_handle(Rc::downgrade(&rc_dyn));
+
+ let mut provider_listing: IndexMap<String, ProviderListingEntry> = IndexMap::new();
+ provider_listing.insert("a".to_string(), ProviderListingEntry { sha256 });
+ rc.borrow_mut().__set_provider_listing(provider_listing);
+ rc.borrow_mut()
+ .__set_providers_url("https://dummy.test.link/to/%package%/file");
+
+ let packages = rc.borrow_mut().__what_provides("a").unwrap();
+
+ assert_eq!(5, packages.len());
+ assert_eq!(
+ vec!["1", "1-alias", "2", "2-alias", "3"],
+ packages.keys().cloned().collect::<Vec<_>>()
+ );
+ let alias = packages["2-alias"].as_alias().unwrap();
+ let aliased: shirabe::package::handle::PackageInterfaceHandle = alias.get_alias_of().into();
+ assert!(packages["2"].ptr_eq(&aliased));
}
-#[ignore = "needs getHttpDownloaderMock test helper (HttpDownloaderMock not ported); real HttpDownloader hits todo!() curl I/O"]
#[test]
fn test_search_with_type() {
- todo!()
+ let (config, _home) = create_config_read_only();
+
+ let result = str_kv(&[(
+ "results",
+ PhpMixed::List(vec![str_kv(&[
+ ("name", PhpMixed::String("foo".to_string())),
+ ("description", PhpMixed::Null),
+ ])]),
+ )]);
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ "http://example.org/packages.json",
+ json_encode(&str_kv(&[(
+ "search",
+ PhpMixed::String("/search.json?q=%query%&type=%type%".to_string()),
+ )])),
+ ),
+ http_body(
+ "http://example.org/search.json?q=foo&type=composer-plugin",
+ json_encode(&result),
+ ),
+ http_body(
+ "http://example.org/search.json?q=foo&type=library",
+ json_encode(&PhpMixed::List(vec![])),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repository = ComposerRepository::new(
+ repo_config("http://example.org"),
+ null_io(),
+ &config,
+ http_downloader,
+ None,
+ )
+ .unwrap();
+
+ let plugin_results = repository
+ .search(
+ "foo".to_string(),
+ SEARCH_FULLTEXT,
+ Some("composer-plugin".to_string()),
+ )
+ .unwrap();
+ assert_eq!(1, plugin_results.len());
+ assert_eq!(
+ Some("foo"),
+ plugin_results[0].get("name").and_then(|v| v.as_string())
+ );
+ assert!(matches!(
+ plugin_results[0].get("description"),
+ Some(PhpMixed::Null)
+ ));
+
+ let library_results = repository
+ .search(
+ "foo".to_string(),
+ SEARCH_FULLTEXT,
+ Some("library".to_string()),
+ )
+ .unwrap();
+ assert!(library_results.is_empty());
}
-#[ignore = "needs getHttpDownloaderMock test helper (HttpDownloaderMock not ported); real HttpDownloader hits todo!() curl I/O"]
#[test]
fn test_search_with_special_chars() {
- todo!()
+ let (config, _home) = create_config_read_only();
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ "http://example.org/packages.json",
+ json_encode(&str_kv(&[(
+ "search",
+ PhpMixed::String("/search.json?q=%query%&type=%type%".to_string()),
+ )])),
+ ),
+ http_body(
+ "http://example.org/search.json?q=foo+bar&type=",
+ json_encode(&PhpMixed::List(vec![])),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repository = ComposerRepository::new(
+ repo_config("http://example.org"),
+ null_io(),
+ &config,
+ http_downloader,
+ None,
+ )
+ .unwrap();
+
+ let results = repository
+ .search("foo bar".to_string(), SEARCH_FULLTEXT, None)
+ .unwrap();
+ assert!(results.is_empty());
}
-#[ignore = "needs getHttpDownloaderMock test helper (HttpDownloaderMock not ported); real HttpDownloader hits todo!() curl I/O"]
#[test]
fn test_search_with_abandoned_packages() {
- todo!()
+ let (config, _home) = create_config_read_only();
+
+ let result = str_kv(&[(
+ "results",
+ PhpMixed::List(vec![
+ str_kv(&[
+ ("name", PhpMixed::String("foo1".to_string())),
+ ("description", PhpMixed::Null),
+ ("abandoned", PhpMixed::Bool(true)),
+ ]),
+ str_kv(&[
+ ("name", PhpMixed::String("foo2".to_string())),
+ ("description", PhpMixed::Null),
+ ("abandoned", PhpMixed::String("bar".to_string())),
+ ]),
+ ]),
+ )]);
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ "http://example.org/packages.json",
+ json_encode(&str_kv(&[(
+ "search",
+ PhpMixed::String("/search.json?q=%query%".to_string()),
+ )])),
+ ),
+ http_body("http://example.org/search.json?q=foo", json_encode(&result)),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repository = ComposerRepository::new(
+ repo_config("http://example.org"),
+ null_io(),
+ &config,
+ http_downloader,
+ None,
+ )
+ .unwrap();
+
+ let results = repository
+ .search("foo".to_string(), SEARCH_FULLTEXT, None)
+ .unwrap();
+
+ assert_eq!(2, results.len());
+ assert_eq!(
+ Some("foo1"),
+ results[0].get("name").and_then(|v| v.as_string())
+ );
+ assert!(matches!(
+ results[0].get("description"),
+ Some(PhpMixed::Null)
+ ));
+ assert!(matches!(
+ results[0].get("abandoned"),
+ Some(PhpMixed::Bool(true))
+ ));
+ assert_eq!(
+ Some("foo2"),
+ results[1].get("name").and_then(|v| v.as_string())
+ );
+ assert!(matches!(
+ results[1].get("description"),
+ Some(PhpMixed::Null)
+ ));
+ assert_eq!(
+ Some("bar"),
+ results[1].get("abandoned").and_then(|v| v.as_string())
+ );
+}
+
+fn canonicalize_url_test_cases() -> Vec<(&'static str, &'static str, &'static str)> {
+ vec![
+ (
+ "https://example.org/path/to/file",
+ "/path/to/file",
+ "https://example.org",
+ ),
+ (
+ "https://example.org/canonic_url",
+ "https://example.org/canonic_url",
+ "https://should-not-see-me.test",
+ ),
+ (
+ "file:///path/to/repository/file",
+ "/path/to/repository/file",
+ "file:///path/to/repository",
+ ),
+ // Repository URL returned unchanged if it is not a URL (BC test).
+ ("invalid_repo_url", "/path/to/file", "invalid_repo_url"),
+ // URLs may contain sequences resembling preg_replace() pattern references
+ // without messing up the result (regression test).
+ (
+ "https://example.org/path/to/unusual_$0_filename",
+ "/path/to/unusual_$0_filename",
+ "https://example.org",
+ ),
+ ]
}
-#[ignore = "needs getMockBuilder HttpDownloader mock plus ReflectionObject getMethod/getProperty to set private url and invoke canonicalizeUrl; no mocking/reflection ported"]
#[test]
fn test_canonicalize_url() {
- todo!()
+ for (expected, url, repository_url) in canonicalize_url_test_cases() {
+ let (config, _home) = create_config();
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) =
+ get_http_downloader_mock(vec![], false, HttpDownloaderMockHandler::default());
+
+ let mut repository = ComposerRepository::new(
+ repo_config(repository_url),
+ null_io(),
+ &config,
+ http_downloader,
+ None,
+ )
+ .unwrap();
+
+ // __construct ensures the repository URL has a protocol, so reset it here in
+ // order to test all cases.
+ repository.__set_url(repository_url);
+
+ assert_eq!(expected, repository.__canonicalize_url(url).unwrap());
+ }
}
-#[ignore = "needs getHttpDownloaderMock test helper (HttpDownloaderMock not ported); real HttpDownloader hits todo!() curl I/O"]
#[test]
fn test_get_provider_names_will_return_partial_package_names() {
- todo!()
+ let (config, _home) = create_config();
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![http_body(
+ "http://example.org/packages.json",
+ json_encode(&str_kv(&[
+ (
+ "providers-lazy-url",
+ PhpMixed::String("/foo/p/%package%.json".to_string()),
+ ),
+ (
+ "packages",
+ str_kv(&[(
+ "foo/bar",
+ str_kv(&[
+ (
+ "dev-branch",
+ str_kv(&[("name", PhpMixed::String("foo/bar".to_string()))]),
+ ),
+ (
+ "v1.0.0",
+ str_kv(&[("name", PhpMixed::String("foo/bar".to_string()))]),
+ ),
+ ]),
+ )]),
+ ),
+ ])),
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repository = ComposerRepository::new(
+ repo_config("http://example.org/packages.json"),
+ null_io(),
+ &config,
+ http_downloader,
+ None,
+ )
+ .unwrap();
+
+ assert_eq!(vec!["foo/bar"], repository.get_package_names(None).unwrap());
}
-#[ignore = "needs getHttpDownloaderMock test helper (HttpDownloaderMock not ported); real HttpDownloader hits todo!() curl I/O"]
#[test]
fn test_get_security_advisories_assert_repository_http_options_are_used() {
- todo!()
+ let (config, _home) = create_config();
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ "https://example.org/packages.json",
+ json_encode(&str_kv(&[
+ (
+ "packages",
+ str_kv(&[(
+ "foo/bar",
+ str_kv(&[
+ (
+ "dev-branch",
+ str_kv(&[("name", PhpMixed::String("foo/bar".to_string()))]),
+ ),
+ (
+ "v1.0.0",
+ str_kv(&[("name", PhpMixed::String("foo/bar".to_string()))]),
+ ),
+ ]),
+ )]),
+ ),
+ (
+ "metadata-url",
+ PhpMixed::String("https://example.org/p2/%package%.json".to_string()),
+ ),
+ (
+ "security-advisories",
+ str_kv(&[(
+ "api-url",
+ PhpMixed::String("https://example.org/security-advisories".to_string()),
+ )]),
+ ),
+ ])),
+ ),
+ http_body(
+ "https://example.org/security-advisories",
+ json_encode(&str_kv(&[("advisories", PhpMixed::List(vec![]))])),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert(
+ "url".to_string(),
+ PhpMixed::String("https://example.org/packages.json".to_string()),
+ );
+ repo_config.insert(
+ "options".to_string(),
+ str_kv(&[("http", str_kv(&[("verify_peer", PhpMixed::Bool(false))]))]),
+ );
+
+ let mut repository =
+ ComposerRepository::new(repo_config, null_io(), &config, http_downloader, None).unwrap();
+
+ let constraint: AnyConstraint =
+ SimpleConstraint::new("=".to_string(), "1.0.0.0".to_string(), None).into();
+ let mut map: IndexMap<String, AnyConstraint> = IndexMap::new();
+ map.insert("foo/bar".to_string(), constraint);
+
+ let result = repository.get_security_advisories(map, false).unwrap();
+ assert!(result.names_found.is_empty());
+ assert!(result.advisories.is_empty());
+}
+
+fn generate_security_advisory(
+ package_name: &str,
+ cve: Option<&str>,
+ affected_versions: &str,
+) -> (String, PhpMixed) {
+ let advisory_id = shirabe_php_shim::uniqid("PKSA-", false);
+ let advisory = str_kv(&[
+ ("advisoryId", PhpMixed::String(advisory_id.clone())),
+ ("packageName", PhpMixed::String(package_name.to_string())),
+ ("remoteId", PhpMixed::String("test".to_string())),
+ ("title", PhpMixed::String("Security Advisory".to_string())),
+ ("link", PhpMixed::Null),
+ (
+ "cve",
+ match cve {
+ Some(c) => PhpMixed::String(c.to_string()),
+ None => PhpMixed::Null,
+ },
+ ),
+ (
+ "affectedVersions",
+ PhpMixed::String(affected_versions.to_string()),
+ ),
+ ("source", PhpMixed::String("Tests".to_string())),
+ (
+ "reportedAt",
+ PhpMixed::String("2024-04-31 12:37:47".to_string()),
+ ),
+ (
+ "composerRepository",
+ PhpMixed::String("Package Repository".to_string()),
+ ),
+ ("severity", PhpMixed::String("high".to_string())),
+ (
+ "sources",
+ PhpMixed::List(vec![str_kv(&[
+ ("name", PhpMixed::String("Security Advisory".to_string())),
+ ("remoteId", PhpMixed::String("test".to_string())),
+ ])]),
+ ),
+ ]);
+ (advisory_id, advisory)
}
-#[ignore = "needs getHttpDownloaderMock test helper (HttpDownloaderMock not ported); real HttpDownloader hits todo!() curl I/O"]
+// Ported and exercising the real getSecurityAdvisories path, but blocked by an unimplemented
+// production shim: constructing a full SecurityAdvisory parses `reportedAt` via
+// shirabe_php_shim::date_create (todo!(): needs the strtotime grammar parser).
+#[ignore = "production todo!(): shirabe_php_shim::date_create unimplemented (reached when parsing advisory reportedAt into a full SecurityAdvisory)"]
#[test]
fn test_get_security_advisories_assert_repository_advisories_is_zero_indexed_array_with_consecutive_keys()
{
- todo!()
+ let (config, _home) = create_config();
+
+ let package_name = "foo/bar";
+ let (advisory1_id, advisory1) =
+ generate_security_advisory(package_name, Some("CVE-1999-1000"), ">=1.0.0,<1.1.0");
+ let (_advisory2_id, advisory2) =
+ generate_security_advisory(package_name, Some("CVE-1999-1000"), ">=2.0.0");
+ let (advisory3_id, advisory3) =
+ generate_security_advisory(package_name, Some("CVE-1999-1000"), ">=1.0.0,<1.1.0");
+
+ let expected_advisory_ids = [advisory1_id, advisory3_id];
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ "https://example.org/packages.json",
+ json_encode(&str_kv(&[
+ (
+ "packages",
+ str_kv(&[(
+ package_name,
+ str_kv(&[
+ (
+ "dev-branch",
+ str_kv(&[("name", PhpMixed::String(package_name.to_string()))]),
+ ),
+ (
+ "v1.0.0",
+ str_kv(&[("name", PhpMixed::String(package_name.to_string()))]),
+ ),
+ ]),
+ )]),
+ ),
+ (
+ "metadata-url",
+ PhpMixed::String("https://example.org/p2/%package%.json".to_string()),
+ ),
+ (
+ "security-advisories",
+ str_kv(&[(
+ "api-url",
+ PhpMixed::String("https://example.org/security-advisories".to_string()),
+ )]),
+ ),
+ ])),
+ ),
+ http_body(
+ "https://example.org/security-advisories",
+ json_encode(&str_kv(&[(
+ "advisories",
+ str_kv(&[(
+ package_name,
+ PhpMixed::List(vec![advisory1, advisory2, advisory3]),
+ )]),
+ )])),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert(
+ "url".to_string(),
+ PhpMixed::String("https://example.org/packages.json".to_string()),
+ );
+ repo_config.insert(
+ "options".to_string(),
+ str_kv(&[("http", str_kv(&[("verify_peer", PhpMixed::Bool(false))]))]),
+ );
+
+ let mut repository =
+ ComposerRepository::new(repo_config, null_io(), &config, http_downloader, None).unwrap();
+
+ let constraint: AnyConstraint =
+ SimpleConstraint::new("=".to_string(), "1.0.0.0".to_string(), None).into();
+ let mut map: IndexMap<String, AnyConstraint> = IndexMap::new();
+ map.insert(package_name.to_string(), constraint);
+
+ let result = repository.get_security_advisories(map, false).unwrap();
+
+ let actual = result.advisories.get(package_name).unwrap();
+ assert_eq!(expected_advisory_ids.len(), actual.len());
+ for (i, expected_id) in expected_advisory_ids.iter().enumerate() {
+ assert_eq!(expected_id, actual[i].advisory_id());
+ }
}