diff options
Diffstat (limited to 'crates/shirabe')
25 files changed, 1434 insertions, 98 deletions
diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs index 7bd65b6..308d557 100644 --- a/crates/shirabe/src/command/config_command.rs +++ b/crates/shirabe/src/command/config_command.rs @@ -823,14 +823,27 @@ impl Command for ConfigCommand { PhpMixed::List(value.as_list().cloned().unwrap_or_default()), ); } else { - // PHP "+" operator on arrays: keep keys from left, fill from right - let mut merged: IndexMap<String, PhpMixed> = - value.as_array().cloned().unwrap_or_default(); - if let Some(cv) = current_value.as_array() { - for (k, v) in cv { - if !merged.contains_key(k) { - merged.insert(k.clone(), v.clone()); - } + // PHP "+" operator on arrays: keep keys from left, fill from right. + // A list participates with its integer indices as keys. + let mut merged: IndexMap<String, PhpMixed> = match &value { + PhpMixed::List(l) => l + .iter() + .enumerate() + .map(|(i, v)| (i.to_string(), v.clone())) + .collect(), + _ => value.as_array().cloned().unwrap_or_default(), + }; + let fill: IndexMap<String, PhpMixed> = match ¤t_value { + PhpMixed::List(l) => l + .iter() + .enumerate() + .map(|(i, v)| (i.to_string(), v.clone())) + .collect(), + _ => current_value.as_array().cloned().unwrap_or_default(), + }; + for (k, v) in fill { + if !merged.contains_key(&k) { + merged.insert(k, v); } } value = PhpMixed::Array(merged); diff --git a/crates/shirabe/src/package/loader/validating_array_loader.rs b/crates/shirabe/src/package/loader/validating_array_loader.rs index 342e1cb..6a85e37 100644 --- a/crates/shirabe/src/package/loader/validating_array_loader.rs +++ b/crates/shirabe/src/package/loader/validating_array_loader.rs @@ -1063,19 +1063,22 @@ impl ValidatingArrayLoader { .and_then(|v| v.as_array()) .cloned() .unwrap_or_default(); - if !section.contains_key("type") { + // Mirror PHP `isset()`, which is false for both missing keys and null values. + let isset = + |key: &str| matches!(section.get(key), Some(v) if !matches!(v, PhpMixed::Null)); + if !isset("type") { self.errors .push(format!("{}.type : must be present", src_type)); } - if !section.contains_key("url") { + if !isset("url") { self.errors .push(format!("{}.url : must be present", src_type)); } - if src_type == "source" && !section.contains_key("reference") { + if src_type == "source" && !isset("reference") { self.errors .push(format!("{}.reference : must be present", src_type)); } - if let Some(type_val) = section.get("type") + if let Some(type_val) = section.get("type").filter(|_| isset("type")) && !is_string(type_val) { self.errors.push(format!( @@ -1084,7 +1087,7 @@ impl ValidatingArrayLoader { get_debug_type(type_val) )); } - if let Some(url_val) = section.get("url") + if let Some(url_val) = section.get("url").filter(|_| isset("url")) && !is_string(url_val) { self.errors.push(format!( @@ -1093,7 +1096,7 @@ impl ValidatingArrayLoader { get_debug_type(url_val) )); } - if let Some(ref_val) = section.get("reference") + if let Some(ref_val) = section.get("reference").filter(|_| isset("reference")) && !is_string(ref_val) && !is_int(ref_val) { @@ -1103,7 +1106,7 @@ impl ValidatingArrayLoader { get_debug_type(ref_val) )); } - if let Some(ref_val) = section.get("reference") { + if let Some(ref_val) = section.get("reference").filter(|_| isset("reference")) { let ref_str = php_to_string(ref_val); if Preg::is_match("{^\\s*-}", &ref_str) { self.errors.push(format!( @@ -1112,7 +1115,7 @@ impl ValidatingArrayLoader { )); } } - if let Some(url_val) = section.get("url") { + if let Some(url_val) = section.get("url").filter(|_| isset("url")) { let url_str = php_to_string(url_val); if Preg::is_match("{^\\s*-}", &url_str) { self.errors.push(format!( diff --git a/crates/shirabe/src/package/root_alias_package.rs b/crates/shirabe/src/package/root_alias_package.rs index 140c96d..4d8bd6e 100644 --- a/crates/shirabe/src/package/root_alias_package.rs +++ b/crates/shirabe/src/package/root_alias_package.rs @@ -92,18 +92,35 @@ impl RootPackageInterface for RootAliasPackage { } fn set_dev_requires(&mut self, dev_requires: IndexMap<String, Link>) { + self.inner.inner.dev_requires = self + .inner + .inner + .replace_self_version_dependencies(dev_requires.clone(), Link::TYPE_DEV_REQUIRE); + self.alias_of.set_dev_requires(dev_requires); } fn set_conflicts(&mut self, conflicts: IndexMap<String, Link>) { + self.inner.inner.conflicts = self + .inner + .inner + .replace_self_version_dependencies(conflicts.clone(), Link::TYPE_CONFLICT); self.alias_of.set_conflicts(conflicts); } fn set_provides(&mut self, provides: IndexMap<String, Link>) { + self.inner.inner.provides = self + .inner + .inner + .replace_self_version_dependencies(provides.clone(), Link::TYPE_PROVIDE); self.alias_of.set_provides(provides); } fn set_replaces(&mut self, replaces: IndexMap<String, Link>) { + self.inner.inner.replaces = self + .inner + .inner + .replace_self_version_dependencies(replaces.clone(), Link::TYPE_REPLACE); self.alias_of.set_replaces(replaces); } diff --git a/crates/shirabe/src/repository/repository_factory.rs b/crates/shirabe/src/repository/repository_factory.rs index 47caf59..9cc4e4d 100644 --- a/crates/shirabe/src/repository/repository_factory.rs +++ b/crates/shirabe/src/repository/repository_factory.rs @@ -4,6 +4,7 @@ use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, UnexpectedValueException, get_debug_type, json_encode, + php_to_string, }; use crate::config::Config; @@ -318,15 +319,11 @@ impl RepositoryFactory { repo: &IndexMap<String, PhpMixed>, existing_repos: &IndexMap<String, T>, ) -> String { - let mut name = match index { - PhpMixed::Int(_) => { - if let Some(url) = repo.get("url").and_then(|v| v.as_string()) { - Preg::replace("{^https?://}i", "", url) - } else { - index.as_string().unwrap_or("").to_string() - } - } - _ => index.as_string().unwrap_or("").to_string(), + let mut name = if matches!(index, PhpMixed::Int(_)) && repo.contains_key("url") { + let url = repo.get("url").and_then(|v| v.as_string()).unwrap_or(""); + Preg::replace("{^https?://}i", "", url) + } else { + php_to_string(index) }; while existing_repos.contains_key(&name) { name.push('2'); diff --git a/crates/shirabe/src/util/forgejo.rs b/crates/shirabe/src/util/forgejo.rs index 4ca4710..3a51be5 100644 --- a/crates/shirabe/src/util/forgejo.rs +++ b/crates/shirabe/src/util/forgejo.rs @@ -124,7 +124,7 @@ impl Forgejo { Err(e) => { let code = e .downcast_ref::<crate::downloader::TransportException>() - .and_then(|te| te.get_status_code()) + .map(|te| te.code) .unwrap_or(0); if [403, 401, 404].contains(&code) { self.io.write_error3( diff --git a/crates/shirabe/src/util/mod.rs b/crates/shirabe/src/util/mod.rs index d34fce5..bd51b66 100644 --- a/crates/shirabe/src/util/mod.rs +++ b/crates/shirabe/src/util/mod.rs @@ -27,6 +27,7 @@ pub mod stream_context_factory; pub mod svn; pub mod sync_helper; pub mod tar; +pub mod tls_helper; pub mod url; pub mod zip; @@ -58,5 +59,6 @@ pub use stream_context_factory::*; pub use svn::*; pub use sync_helper::*; pub use tar::*; +pub use tls_helper::*; pub use url::*; pub use zip::*; diff --git a/crates/shirabe/src/util/tls_helper.rs b/crates/shirabe/src/util/tls_helper.rs new file mode 100644 index 0000000..6422be3 --- /dev/null +++ b/crates/shirabe/src/util/tls_helper.rs @@ -0,0 +1,149 @@ +//! ref: composer/src/Composer/Util/TlsHelper.php + +use shirabe_external_packages::composer::ca_bundle::ca_bundle::CaBundle; +use shirabe_external_packages::composer::pcre::Preg; +use shirabe_php_shim::{ + PhpMixed, ltrim, preg_quote, str_replace, strtolower, substr, substr_count, +}; + +/// Extracted certificate names. Mirrors PHP's `array{cn: string, san: string[]}`. +#[derive(Debug, Clone)] +pub struct CertificateNames { + pub cn: String, + pub san: Vec<String>, +} + +/// Match hostname against a certificate. +/// +/// @deprecated Use composer/ca-bundle and composer/composer 2.2 if you still need PHP 5 +/// compatibility, this class will be removed in Composer 3.0 +#[derive(Debug)] +pub struct TlsHelper; + +impl TlsHelper { + /// Match hostname against a certificate. Sets `cn` to the common name of the + /// certificate iff a match is found. + pub fn check_certificate_host( + certificate: &PhpMixed, + hostname: &str, + cn: &mut Option<String>, + ) -> bool { + let names = Self::get_certificate_names(certificate); + + let Some(names) = names else { + return false; + }; + + let mut combined_names = names.san.clone(); + combined_names.push(names.cn.clone()); + let hostname = strtolower(hostname); + + for cert_name in &combined_names { + let matcher = Self::cert_name_matcher(cert_name); + + if let Some(matcher) = matcher + && matcher(&hostname) + { + *cn = Some(names.cn.clone()); + + return true; + } + } + + false + } + + /// Extract DNS names out of an X.509 certificate. + pub fn get_certificate_names(certificate: &PhpMixed) -> Option<CertificateNames> { + let info: Option<&PhpMixed> = if certificate.as_array().is_some() { + Some(certificate) + } else if CaBundle::is_openssl_parse_safe() { + // TODO(phase-c): openssl_x509_parse on a PEM string certificate. + todo!("openssl_x509_parse for non-array certificates") + } else { + None + }; + + let info = info?.as_array()?; + + let common_name = info + .get("subject") + .and_then(|s| s.as_array()) + .and_then(|s| s.get("commonName")) + .and_then(|c| c.as_string()); + + let common_name = strtolower(common_name?); + let mut subject_alt_names: Vec<String> = Vec::new(); + + if let Some(san) = info + .get("extensions") + .and_then(|e| e.as_array()) + .and_then(|e| e.get("subjectAltName")) + .and_then(|s| s.as_string()) + { + let split = Preg::split("{\\s*,\\s*}", san); + subject_alt_names = split + .into_iter() + .filter_map(|name| { + if name.starts_with("DNS:") { + Some(strtolower(<rim(&substr(&name, 4, None), None))) + } else { + None + } + }) + .collect(); + } + + Some(CertificateNames { + cn: common_name, + san: subject_alt_names, + }) + } + + /// Get the certificate pin. + pub fn get_certificate_fingerprint(_certificate: &str) -> String { + todo!("openssl public key extraction and sha1 fingerprint") + } + + /// Test if it is safe to use the PHP function openssl_x509_parse(). + pub fn is_openssl_parse_safe() -> bool { + CaBundle::is_openssl_parse_safe() + } + + /// Convert certificate name into matching function. + fn cert_name_matcher(cert_name: &str) -> Option<Box<dyn Fn(&str) -> bool>> { + let wildcards = substr_count(cert_name, "*"); + + if wildcards == 0 { + // Literal match. + let cert_name = cert_name.to_string(); + return Some(Box::new(move |hostname: &str| hostname == cert_name)); + } + + if wildcards == 1 { + let components: Vec<&str> = cert_name.split('.').collect(); + + if components.len() < 3 { + // Must have 3+ components + return None; + } + + let first_component = components[0]; + + // Wildcard must be the last character. + if !first_component.ends_with('*') { + return None; + } + + let mut wildcard_regex = preg_quote(cert_name, None); + wildcard_regex = str_replace("\\*", "[a-z0-9-]+", &wildcard_regex); + let wildcard_regex = format!("{{^{}$}}", wildcard_regex); + + return Some(Box::new(move |hostname: &str| { + Preg::is_match(&wildcard_regex, hostname) + })); + } + + None + } +} diff --git a/crates/shirabe/tests/command/about_command_test.rs b/crates/shirabe/tests/command/about_command_test.rs index 591fbfa..bb726cf 100644 --- a/crates/shirabe/tests/command/about_command_test.rs +++ b/crates/shirabe/tests/command/about_command_test.rs @@ -1,7 +1,33 @@ //! ref: composer/tests/Composer/Test/Command/AboutCommandTest.php +use crate::test_case::{RunOptions, get_application_tester}; +use serial_test::serial; +use shirabe::composer; +use shirabe_php_shim::PhpMixed; + #[test] -#[ignore = "missing get_application_tester (ApplicationTester) infrastructure and Application::get_display/set_auto_exit"] +#[serial] fn test_about() { - todo!() + let composer_version = composer::get_version(); + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![(PhpMixed::from("command"), PhpMixed::from("about"))], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); + + assert!(app_tester.get_display().contains(&format!( + "Composer - Dependency Manager for PHP - version {composer_version}" + ))); + + assert!(app_tester.get_display().contains( + "Composer is a dependency manager tracking local dependencies of your projects and libraries." + )); + assert!( + app_tester + .get_display() + .contains("See https://getcomposer.org/ for more information.") + ); } diff --git a/crates/shirabe/tests/command/bump_command_test.rs b/crates/shirabe/tests/command/bump_command_test.rs index 96c8baf..7accb75 100644 --- a/crates/shirabe/tests/command/bump_command_test.rs +++ b/crates/shirabe/tests/command/bump_command_test.rs @@ -4,7 +4,10 @@ use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; use serial_test::serial; use shirabe_php_shim::PhpMixed; -#[ignore = "missing TestCase::create_installed_json / create_composer_lock infrastructure and full bump flow (require_composer reaches the network)"] +#[ignore = "needs create_installed_json / create_composer_lock helpers \ + (InstalledFilesystemRepository / Locker::set_lock_data), and BumpCommand::execute calls \ + require_composer -> Factory, reaching ProcessExecutor (git) -> shirabe-php-shim \ + stream_set_blocking (stream.rs todo!(), requires fcntl(2))"] #[test] fn test_bump() { todo!() diff --git a/crates/shirabe/tests/command/check_platform_reqs_command_test.rs b/crates/shirabe/tests/command/check_platform_reqs_command_test.rs index 9cbba58..47f89b9 100644 --- a/crates/shirabe/tests/command/check_platform_reqs_command_test.rs +++ b/crates/shirabe/tests/command/check_platform_reqs_command_test.rs @@ -1,19 +1,25 @@ //! ref: composer/tests/Composer/Test/Command/CheckPlatformReqsCommandTest.php #[test] -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "needs create_installed_json / create_composer_lock helpers (InstalledFilesystemRepository \ + / Locker::set_lock_data), and require_composer -> Factory reaches ProcessExecutor (git) \ + -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2))"] fn test_platform_reqs_are_satisfied() { todo!() } #[test] -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "check-platform-reqs calls require_composer -> Factory, reaching ProcessExecutor (git) \ + -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2)) before \ + the missing-lockfile LogicException can be thrown"] fn test_exception_thrown_if_no_lockfile_found() { todo!() } #[test] -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "needs create_installed_json / create_composer_lock helpers (InstalledFilesystemRepository \ + / Locker::set_lock_data), and require_composer -> Factory reaches ProcessExecutor (git) \ + -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2))"] fn test_failed_platform_requirement() { todo!() } diff --git a/crates/shirabe/tests/command/clear_cache_command_test.rs b/crates/shirabe/tests/command/clear_cache_command_test.rs index 4ad04dd..b185579 100644 --- a/crates/shirabe/tests/command/clear_cache_command_test.rs +++ b/crates/shirabe/tests/command/clear_cache_command_test.rs @@ -1,6 +1,9 @@ //! ref: composer/tests/Composer/Test/Command/ClearCacheCommandTest.php +use crate::test_case::{RunOptions, get_application_tester}; +use serial_test::serial; use shirabe::util::platform::Platform; +use shirabe_php_shim::PhpMixed; fn tear_down() { // --no-cache triggers the env to change so make sure the env is cleaned up after these tests run @@ -16,25 +19,76 @@ impl Drop for TearDown { } #[test] -#[ignore = "ApplicationTester / get_application_tester helper not implemented"] +#[serial] fn test_clear_cache_command_success() { let _tear_down = TearDown; - todo!() + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![(PhpMixed::from("command"), PhpMixed::from("clear-cache"))], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!(0, app_tester.get_status_code()); + + let output = app_tester.get_display(); + assert!( + output.contains("All caches cleared."), + "expected output to contain 'All caches cleared.', got: {:?}", + output, + ); } #[test] -#[ignore = "ApplicationTester / get_application_tester helper not implemented"] +#[serial] fn test_clear_cache_command_with_option_garbage_collection() { let _tear_down = TearDown; - todo!() + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("clear-cache")), + (PhpMixed::from("--gc"), PhpMixed::Bool(true)), + ], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!(0, app_tester.get_status_code()); + + let output = app_tester.get_display(); + assert!( + output.contains("All caches garbage-collected."), + "expected output to contain 'All caches garbage-collected.', got: {:?}", + output, + ); } #[test] -#[ignore = "ApplicationTester / get_application_tester helper not implemented"] +#[serial] fn test_clear_cache_command_with_option_no_cache() { let _tear_down = TearDown; - todo!() + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("clear-cache")), + (PhpMixed::from("--no-cache"), PhpMixed::Bool(true)), + ], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!(0, app_tester.get_status_code()); + + let output = app_tester.get_display(); + assert!( + output.contains("Cache is not enabled"), + "expected output to contain 'Cache is not enabled', got: {:?}", + output, + ); } diff --git a/crates/shirabe/tests/command/config_command_test.rs b/crates/shirabe/tests/command/config_command_test.rs index 70e6a97..f43322d 100644 --- a/crates/shirabe/tests/command/config_command_test.rs +++ b/crates/shirabe/tests/command/config_command_test.rs @@ -1,31 +1,508 @@ //! ref: composer/tests/Composer/Test/Command/ConfigCommandTest.php +use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; +use serial_test::serial; +use shirabe_php_shim::PhpMixed; + +/// `['command' => 'config'] + $command`, with the command name prepended. +fn config_input(command: Vec<(PhpMixed, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + let mut input = vec![(PhpMixed::from("command"), PhpMixed::from("config"))]; + input.extend(command); + input +} + +fn key(setting_key: &str) -> (PhpMixed, PhpMixed) { + (PhpMixed::from("setting-key"), PhpMixed::from(setting_key)) +} + +fn value(values: &[&str]) -> (PhpMixed, PhpMixed) { + ( + PhpMixed::from("setting-value"), + PhpMixed::List(values.iter().map(|v| PhpMixed::from(*v)).collect()), + ) +} + +fn flag(name: &str) -> (PhpMixed, PhpMixed) { + (PhpMixed::from(name), PhpMixed::Bool(true)) +} + +/// Reads CWD's composer.json as a `serde_json::Value` (mirrors PHP's `json_decode(..., true)`). +fn read_composer_json() -> serde_json::Value { + let contents = std::fs::read_to_string("composer.json").unwrap(); + serde_json::from_str(&contents).unwrap() +} + +struct UpdateCase { + name: &'static str, + before: serde_json::Value, + command: Vec<(PhpMixed, PhpMixed)>, + expected: serde_json::Value, +} + #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] fn test_config_updates() { - todo!() + let cases: Vec<UpdateCase> = vec![ + UpdateCase { + name: "set scripts", + before: serde_json::json!({}), + command: vec![key("scripts.test"), value(&["foo bar"])], + expected: serde_json::json!({"scripts": {"test": "foo bar"}}), + }, + UpdateCase { + name: "unset scripts", + before: serde_json::json!({"scripts": {"test": "foo bar", "lala": "baz"}}), + command: vec![key("scripts.lala"), flag("--unset")], + expected: serde_json::json!({"scripts": {"test": "foo bar"}}), + }, + UpdateCase { + name: "set single config with bool normalizer", + before: serde_json::json!({}), + command: vec![key("use-github-api"), value(&["1"])], + expected: serde_json::json!({"config": {"use-github-api": true}}), + }, + UpdateCase { + name: "set multi config", + before: serde_json::json!({}), + command: vec![key("github-protocols"), value(&["https", "git"])], + expected: serde_json::json!({"config": {"github-protocols": ["https", "git"]}}), + }, + UpdateCase { + name: "set version", + before: serde_json::json!({}), + command: vec![key("version"), value(&["1.0.0"])], + expected: serde_json::json!({"version": "1.0.0"}), + }, + UpdateCase { + name: "unset version", + before: serde_json::json!({"version": "1.0.0"}), + command: vec![key("version"), flag("--unset")], + expected: serde_json::json!({}), + }, + UpdateCase { + name: "unset arbitrary property", + before: serde_json::json!({"random-prop": "1.0.0"}), + command: vec![key("random-prop"), flag("--unset")], + expected: serde_json::json!({}), + }, + UpdateCase { + name: "set preferred-install", + before: serde_json::json!({}), + command: vec![key("preferred-install.foo/*"), value(&["source"])], + expected: serde_json::json!({"config": {"preferred-install": {"foo/*": "source"}}}), + }, + UpdateCase { + name: "unset preferred-install", + before: serde_json::json!({"config": {"preferred-install": {"foo/*": "source"}}}), + command: vec![key("preferred-install.foo/*"), flag("--unset")], + expected: serde_json::json!({"config": {"preferred-install": {}}}), + }, + UpdateCase { + name: "unset platform", + before: serde_json::json!({"config": {"platform": {"php": "7.2.5"}, "platform-check": false}}), + command: vec![key("platform.php"), flag("--unset")], + expected: serde_json::json!({"config": {"platform": {}, "platform-check": false}}), + }, + UpdateCase { + name: "set extra with merge", + before: serde_json::json!({}), + command: vec![ + key("extra.patches.foo/bar"), + value(&["{\"123\":\"value\"}"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"extra": {"patches": {"foo/bar": {"123": "value"}}}}), + }, + UpdateCase { + name: "combine extra with merge", + before: serde_json::json!({"extra": {"patches": {"foo/bar": {"5": "oldvalue"}}}}), + command: vec![ + key("extra.patches.foo/bar"), + value(&["{\"123\":\"value\"}"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"extra": {"patches": {"foo/bar": {"123": "value", "5": "oldvalue"}}}}), + }, + UpdateCase { + name: "combine extra with list", + before: serde_json::json!({"extra": {"patches": {"foo/bar": ["oldvalue"]}}}), + command: vec![ + key("extra.patches.foo/bar"), + value(&["{\"123\":\"value\"}"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"extra": {"patches": {"foo/bar": {"123": "value", "0": "oldvalue"}}}}), + }, + UpdateCase { + name: "overwrite extra with merge", + before: serde_json::json!({"extra": {"patches": {"foo/bar": {"123": "oldvalue"}}}}), + command: vec![ + key("extra.patches.foo/bar"), + value(&["{\"123\":\"value\"}"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"extra": {"patches": {"foo/bar": {"123": "value"}}}}), + }, + UpdateCase { + name: "unset autoload", + before: serde_json::json!({"autoload": {"psr-4": ["test"], "classmap": ["test"]}}), + command: vec![key("autoload.psr-4"), flag("--unset")], + expected: serde_json::json!({"autoload": {"classmap": ["test"]}}), + }, + UpdateCase { + name: "unset autoload-dev", + before: serde_json::json!({"autoload-dev": {"psr-4": ["test"], "classmap": ["test"]}}), + command: vec![key("autoload-dev.psr-4"), flag("--unset")], + expected: serde_json::json!({"autoload-dev": {"classmap": ["test"]}}), + }, + UpdateCase { + name: "set audit.ignore-unreachable", + before: serde_json::json!({}), + command: vec![key("audit.ignore-unreachable"), value(&["true"])], + expected: serde_json::json!({"config": {"audit": {"ignore-unreachable": true}}}), + }, + UpdateCase { + name: "set audit.block-insecure", + before: serde_json::json!({}), + command: vec![key("audit.block-insecure"), value(&["false"])], + expected: serde_json::json!({"config": {"audit": {"block-insecure": false}}}), + }, + UpdateCase { + name: "set audit.block-abandoned", + before: serde_json::json!({}), + command: vec![key("audit.block-abandoned"), value(&["true"])], + expected: serde_json::json!({"config": {"audit": {"block-abandoned": true}}}), + }, + UpdateCase { + name: "unset audit.ignore-unreachable", + before: serde_json::json!({"config": {"audit": {"ignore-unreachable": true}}}), + command: vec![key("audit.ignore-unreachable"), flag("--unset")], + expected: serde_json::json!({"config": {"audit": {}}}), + }, + UpdateCase { + name: "set audit.ignore-severity", + before: serde_json::json!({}), + command: vec![key("audit.ignore-severity"), value(&["low", "medium"])], + expected: serde_json::json!({"config": {"audit": {"ignore-severity": ["low", "medium"]}}}), + }, + UpdateCase { + name: "set audit.ignore as array", + before: serde_json::json!({}), + command: vec![ + key("audit.ignore"), + value(&["[\"CVE-2024-1234\",\"GHSA-xxxx-yyyy\"]"]), + flag("--json"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore": ["CVE-2024-1234", "GHSA-xxxx-yyyy"]}}}), + }, + UpdateCase { + name: "set audit.ignore as object", + before: serde_json::json!({}), + command: vec![ + key("audit.ignore"), + value(&[ + "{\"CVE-2024-1234\":\"False positive\",\"GHSA-xxxx-yyyy\":\"Not applicable\"}", + ]), + flag("--json"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore": {"CVE-2024-1234": "False positive", "GHSA-xxxx-yyyy": "Not applicable"}}}}), + }, + UpdateCase { + name: "merge audit.ignore array", + before: serde_json::json!({"config": {"audit": {"ignore": ["CVE-2024-1234"]}}}), + command: vec![ + key("audit.ignore"), + value(&["[\"CVE-2024-5678\"]"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore": ["CVE-2024-1234", "CVE-2024-5678"]}}}), + }, + UpdateCase { + name: "merge audit.ignore object", + before: serde_json::json!({"config": {"audit": {"ignore": {"CVE-2024-1234": "Old reason"}}}}), + command: vec![ + key("audit.ignore"), + value(&["{\"CVE-2024-5678\":\"New advisory\"}"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore": {"CVE-2024-5678": "New advisory", "CVE-2024-1234": "Old reason"}}}}), + }, + UpdateCase { + name: "overwrite audit.ignore key with merge", + before: serde_json::json!({"config": {"audit": {"ignore": {"CVE-2024-1234": "Old reason"}}}}), + command: vec![ + key("audit.ignore"), + value(&["{\"CVE-2024-1234\":\"New reason\"}"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore": {"CVE-2024-1234": "New reason"}}}}), + }, + UpdateCase { + name: "set audit.ignore-abandoned as array", + before: serde_json::json!({}), + command: vec![ + key("audit.ignore-abandoned"), + value(&["[\"vendor/package1\",\"vendor/package2\"]"]), + flag("--json"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore-abandoned": ["vendor/package1", "vendor/package2"]}}}), + }, + UpdateCase { + name: "set audit.ignore-abandoned as object", + before: serde_json::json!({}), + command: vec![ + key("audit.ignore-abandoned"), + value(&[ + "{\"vendor/package1\":\"Still maintained\",\"vendor/package2\":\"Fork available\"}", + ]), + flag("--json"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore-abandoned": {"vendor/package1": "Still maintained", "vendor/package2": "Fork available"}}}}), + }, + UpdateCase { + name: "merge audit.ignore-abandoned array", + before: serde_json::json!({"config": {"audit": {"ignore-abandoned": ["vendor/package1"]}}}), + command: vec![ + key("audit.ignore-abandoned"), + value(&["[\"vendor/package2\"]"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore-abandoned": ["vendor/package1", "vendor/package2"]}}}), + }, + UpdateCase { + name: "merge audit.ignore-abandoned object", + before: serde_json::json!({"config": {"audit": {"ignore-abandoned": {"vendor/package1": "Old reason"}}}}), + command: vec![ + key("audit.ignore-abandoned"), + value(&["{\"vendor/package2\":\"New reason\"}"]), + flag("--json"), + flag("--merge"), + ], + expected: serde_json::json!({"config": {"audit": {"ignore-abandoned": {"vendor/package2": "New reason", "vendor/package1": "Old reason"}}}}), + }, + UpdateCase { + name: "unset audit.ignore", + before: serde_json::json!({"config": {"audit": {"ignore": ["CVE-2024-1234"]}}}), + command: vec![key("audit.ignore"), flag("--unset")], + expected: serde_json::json!({"config": {"audit": {}}}), + }, + UpdateCase { + name: "unset audit.ignore-abandoned", + before: serde_json::json!({"config": {"audit": {"ignore-abandoned": ["vendor/package1"]}}}), + command: vec![key("audit.ignore-abandoned"), flag("--unset")], + expected: serde_json::json!({"config": {"audit": {}}}), + }, + ]; + + for case in cases { + let _tear_down = init_temp_composer(Some(&case.before), None, None, false); + + let mut app_tester = get_application_tester(); + app_tester + .run(config_input(case.command), RunOptions::default()) + .unwrap_or_else(|e| panic!("case {:?}: run failed: {:?}", case.name, e)); + + assert_eq!( + 0, + app_tester.get_status_code(), + "case {:?}: display: {}", + case.name, + app_tester.get_display() + ); + + assert_eq!(case.expected, read_composer_json(), "case {:?}", case.name); + } +} + +struct ReadCase { + name: &'static str, + composer_json: serde_json::Value, + command: Vec<(PhpMixed, PhpMixed)>, + expected: &'static str, } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] fn test_config_reads() { - todo!() + let cases: Vec<ReadCase> = vec![ + ReadCase { + name: "read description", + composer_json: serde_json::json!({"description": "foo bar"}), + command: vec![key("description")], + expected: "foo bar", + }, + ReadCase { + name: "read vendor-dir with source", + composer_json: serde_json::json!({"config": {"vendor-dir": "lala"}}), + command: vec![key("vendor-dir"), flag("--source")], + expected: "lala (./composer.json)", + }, + ReadCase { + name: "read default vendor-dir", + composer_json: serde_json::json!({}), + command: vec![key("vendor-dir")], + expected: "vendor", + }, + ReadCase { + name: "read repos by named key", + composer_json: serde_json::json!({"repositories": {"foo": {"type": "vcs", "url": "https://example.org"}, "packagist.org": {"type": "composer", "url": "https://repo.packagist.org"}}}), + command: vec![key("repositories.foo")], + expected: "{\"type\":\"vcs\",\"url\":\"https://example.org\"}", + }, + ReadCase { + name: "read all repos includes the default packagist", + composer_json: serde_json::json!({"repositories": {"foo": {"type": "vcs", "url": "https://example.org"}, "packagist.org": {"type": "composer", "url": "https://repo.packagist.org"}}}), + command: vec![key("repos")], + expected: "{\"foo\":{\"type\":\"vcs\",\"url\":\"https://example.org\"},\"packagist.org\":{\"type\":\"composer\",\"url\":\"https://repo.packagist.org\"}}", + }, + ReadCase { + name: "read all repos does not include the disabled packagist", + composer_json: serde_json::json!({"repositories": {"foo": {"type": "vcs", "url": "https://example.org"}, "packagist.org": false}}), + command: vec![key("repos")], + expected: "{\"foo\":{\"type\":\"vcs\",\"url\":\"https://example.org\"}}", + }, + ]; + + for case in cases { + let _tear_down = init_temp_composer(Some(&case.composer_json), None, None, false); + + let mut app_tester = get_application_tester(); + app_tester + .run(config_input(case.command), RunOptions::default()) + .unwrap_or_else(|e| panic!("case {:?}: run failed: {:?}", case.name, e)); + + assert_eq!(0, app_tester.get_status_code(), "case {:?}", case.name); + + assert_eq!( + case.expected, + app_tester.get_display().trim(), + "case {:?}", + case.name + ); + + // The composer.json should not be modified by config reads. + assert_eq!( + case.composer_json, + read_composer_json(), + "case {:?}: composer.json must not be modified by config reads", + case.name + ); + } } +/// ref: provideConfigReads 'read repos by numeric index'. +/// +/// Split out from the rest because list-form `repositories` makes `Config::all()` normalize the +/// repository through RepositoryFactory, which instantiates a repository and reaches +/// shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2)). #[test] -#[ignore = "requires ApplicationTester (getApplicationTester) harness (not implemented)"] +#[serial] +#[ignore = "list-form repositories drive Config::all -> RepositoryFactory, reaching \ + shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2))"] +fn test_config_reads_repos_by_numeric_index() { + let composer_json = serde_json::json!({"repositories": [{"type": "vcs", "url": "https://example.org"}, {"type": "composer", "url": "https://repo.packagist.org"}]}); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, false); + + let mut app_tester = get_application_tester(); + app_tester + .run(config_input(vec![key("repos.0")]), RunOptions::default()) + .unwrap(); + + assert_eq!(0, app_tester.get_status_code()); + assert_eq!( + "{\"type\":\"vcs\",\"url\":\"https://example.org\"}", + app_tester.get_display().trim() + ); +} + +#[test] +#[serial] fn test_config_throws_for_invalid_arg_combination() { - todo!() + let mut app_tester = get_application_tester(); + let result = app_tester.run( + config_input(vec![ + ( + PhpMixed::from("--file"), + PhpMixed::from("alt.composer.json"), + ), + flag("--global"), + ]), + RunOptions::default(), + ); + + let err = result.expect_err("expected RuntimeException"); + assert!( + err.to_string() + .contains("--file and --global can not be combined"), + "got: {:?}", + err + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] +#[ignore = "the command correctly throws, but Application::do_run's exception path calls \ + hint_common_errors -> get_composer (a temp composer.json exists here), which reaches \ + ProcessExecutor (git) -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), \ + requires fcntl(2))"] fn test_config_throws_for_invalid_severity() { - todo!() + let _tear_down = init_temp_composer(Some(&serde_json::json!({})), None, None, false); + + let mut app_tester = get_application_tester(); + let result = app_tester.run( + config_input(vec![ + key("audit.ignore-severity"), + value(&["low", "invalid"]), + ]), + RunOptions::default(), + ); + + let err = result.expect_err("expected RuntimeException"); + assert!( + err.to_string() + .contains("valid severities include: low, medium, high, critical"), + "got: {:?}", + err + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] +#[ignore = "the command correctly throws, but Application::do_run's exception path calls \ + hint_common_errors -> get_composer (a temp composer.json exists here), which reaches \ + ProcessExecutor (git) -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), \ + requires fcntl(2))"] fn test_config_throws_when_merging_array_with_object() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({"config": {"audit": {"ignore": ["CVE-2024-1234"]}}})), + None, + None, + false, + ); + + let mut app_tester = get_application_tester(); + let result = app_tester.run( + config_input(vec![ + key("audit.ignore"), + value(&["{\"CVE-2024-5678\":\"reason\"}"]), + flag("--json"), + flag("--merge"), + ]), + RunOptions::default(), + ); + + let err = result.expect_err("expected RuntimeException"); + assert!( + err.to_string().contains("Cannot merge array and object"), + "got: {:?}", + err + ); } diff --git a/crates/shirabe/tests/command/diagnose_command_test.rs b/crates/shirabe/tests/command/diagnose_command_test.rs index a5a6c8b..a8e6c16 100644 --- a/crates/shirabe/tests/command/diagnose_command_test.rs +++ b/crates/shirabe/tests/command/diagnose_command_test.rs @@ -1,13 +1,15 @@ //! ref: composer/tests/Composer/Test/Command/DiagnoseCommandTest.php #[test] -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "diagnose checks live http/https connectivity to packagist and the github.com rate \ + limit, so it requires real network access"] fn test_cmd_fail() { todo!() } #[test] -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "diagnose checks live http/https connectivity to packagist and the github.com rate \ + limit, so it requires real network access"] fn test_cmd_success() { todo!() } diff --git a/crates/shirabe/tests/command/home_command_test.rs b/crates/shirabe/tests/command/home_command_test.rs index b8b67d7..2c8cb6b 100644 --- a/crates/shirabe/tests/command/home_command_test.rs +++ b/crates/shirabe/tests/command/home_command_test.rs @@ -1,7 +1,10 @@ //! ref: composer/tests/Composer/Test/Command/HomeCommandTest.php #[test] -#[ignore = "init_temp_composer / create_installed_json / get_application_tester (ApplicationTester) helpers not implemented"] +#[ignore = "needs create_installed_json helper (InstalledFilesystemRepository) plus \ + setHomepage on CompletePackage, and HomeCommand::initialize_repos calls try_composer \ + -> Factory, reaching ProcessExecutor (git) -> shirabe-php-shim stream_set_blocking \ + (stream.rs todo!(), requires fcntl(2))"] fn test_home_command_with_show_flag() { todo!() } diff --git a/crates/shirabe/tests/command/validate_command_test.rs b/crates/shirabe/tests/command/validate_command_test.rs index b306100..31af4be 100644 --- a/crates/shirabe/tests/command/validate_command_test.rs +++ b/crates/shirabe/tests/command/validate_command_test.rs @@ -1,25 +1,167 @@ //! ref: composer/tests/Composer/Test/Command/ValidateCommandTest.php +use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; +use serial_test::serial; +use shirabe::util::platform::Platform; +use shirabe_php_shim::PhpMixed; + +/// ref: ValidateCommandTest::MINIMAL_VALID_CONFIGURATION +fn minimal_valid_configuration() -> serde_json::Value { + serde_json::json!({ + "name": "test/suite", + "type": "library", + "description": "A generical test suite", + "license": "MIT", + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "require": {"dep/pkg": "^1"}}, + {"name": "dep/pkg", "version": "1.0.0"}, + {"name": "dep/pkg", "version": "1.0.1"}, + {"name": "dep/pkg", "version": "1.0.2"} + ] + } + }, + "require": { + "root/req": "1.*" + } + }) +} + +fn validate_input(command: Vec<(PhpMixed, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + let mut input = vec![(PhpMixed::from("command"), PhpMixed::from("validate"))]; + input.extend(command); + input +} + +struct ValidateCase { + name: &'static str, + composer_json: serde_json::Value, + command: Vec<(PhpMixed, PhpMixed)>, + expected: &'static str, +} + +/// ref: provideValidateTests +fn provide_validate_tests() -> Vec<ValidateCase> { + // $publishDataStripped = array_diff_key(MINIMAL_VALID_CONFIGURATION, ['name','type','description','license']) + let publish_data_stripped = serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "require": {"dep/pkg": "^1"}}, + {"name": "dep/pkg", "version": "1.0.0"}, + {"name": "dep/pkg", "version": "1.0.1"}, + {"name": "dep/pkg", "version": "1.0.2"} + ] + } + }, + "require": { + "root/req": "1.*" + } + }); + + vec![ + ValidateCase { + name: "validation passing", + composer_json: minimal_valid_configuration(), + command: vec![], + expected: "<warning>Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version</warning>\n<warning>Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version</warning>\n./composer.json is valid", + }, + ValidateCase { + name: "passing but with warnings", + composer_json: publish_data_stripped.clone(), + command: vec![], + expected: "./composer.json is valid for simple usage with Composer but has\nstrict errors that make it unable to be published as a package\n<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>\n# Publish errors\n- name : The property name is required\n- description : The property description is required\n<warning># General warnings</warning>\n- No license specified, it is recommended to do so. For closed-source software you may use \"proprietary\" as license.", + }, + ValidateCase { + name: "passing without publish-check", + composer_json: publish_data_stripped, + command: vec![(PhpMixed::from("--no-check-publish"), PhpMixed::Bool(true))], + expected: "./composer.json is valid, but with a few warnings\n<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>\n<warning># General warnings</warning>\n- No license specified, it is recommended to do so. For closed-source software you may use \"proprietary\" as license.", + }, + ] +} + #[test] -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester with run/get_display) infrastructure"] +#[serial] +#[ignore = "validate creates a Composer instance (create_composer_instance -> Factory) which \ + reaches ProcessExecutor (git) -> shirabe-php-shim stream_set_blocking (stream.rs \ + todo!(), requires fcntl(2))"] fn test_validate() { - todo!() + for case in provide_validate_tests() { + let _tear_down = init_temp_composer(Some(&case.composer_json), None, None, true); + + let mut app_tester = get_application_tester(); + app_tester + .run(validate_input(case.command), RunOptions::default()) + .unwrap_or_else(|e| panic!("case {:?}: run failed: {:?}", case.name, e)); + + assert_eq!( + case.expected.trim(), + app_tester.get_display().trim(), + "case {:?}", + case.name + ); + } } #[test] -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester with run/get_display) infrastructure"] +#[serial] fn test_validate_on_file_issues() { - todo!() + let tear_down = init_temp_composer(Some(&minimal_valid_configuration()), None, None, true); + std::fs::remove_file(tear_down.working_dir().join("composer.json")).unwrap(); + + let mut app_tester = get_application_tester(); + app_tester + .run(validate_input(vec![]), RunOptions::default()) + .unwrap(); + + assert_eq!( + "./composer.json not found.", + app_tester.get_display().trim() + ); + + drop(tear_down); } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "validate with a lock file creates a Composer instance and queries the Locker, reaching \ + ProcessExecutor (git) -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), \ + requires fcntl(2))"] fn test_with_composer_lock() { todo!() } #[test] -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester with run/get_display/get_status_code) infrastructure"] +#[serial] fn test_unaccessible_file() { - todo!() + if Platform::is_windows() { + // ref: $this->markTestSkipped('Does not run on windows'); + return; + } + if shirabe_php_shim::function_exists("posix_getuid") && shirabe_php_shim::posix_getuid() == 0 { + // ref: $this->markTestSkipped('Cannot run as root'); + return; + } + + let tear_down = init_temp_composer(Some(&minimal_valid_configuration()), None, None, true); + let composer_json = tear_down.working_dir().join("composer.json"); + shirabe_php_shim::chmod(&composer_json.to_string_lossy(), 0o200); + + let mut app_tester = get_application_tester(); + app_tester + .run(validate_input(vec![]), RunOptions::default()) + .unwrap(); + + assert_eq!( + "./composer.json is not readable.", + app_tester.get_display().trim() + ); + assert_eq!(3, app_tester.get_status_code()); + + shirabe_php_shim::chmod(&composer_json.to_string_lossy(), 0o700); + drop(tear_down); } diff --git a/crates/shirabe/tests/package/loader/root_package_loader_test.rs b/crates/shirabe/tests/package/loader/root_package_loader_test.rs index e78967e..23c8beb 100644 --- a/crates/shirabe/tests/package/loader/root_package_loader_test.rs +++ b/crates/shirabe/tests/package/loader/root_package_loader_test.rs @@ -36,7 +36,7 @@ fn http_downloader( } #[test] -#[ignore] +#[ignore = "process_executor.enable_async() drives the async stream path, which calls stream_set_blocking (fcntl(2) todo!() in shirabe-php-shim::stream)"] fn test_stability_flags_parsing() { let io = null_io(); let config = Rc::new(RefCell::new(Config::new(true, None))); diff --git a/crates/shirabe/tests/package/loader/validating_array_loader_test.rs b/crates/shirabe/tests/package/loader/validating_array_loader_test.rs index db5ff07..2e614ee 100644 --- a/crates/shirabe/tests/package/loader/validating_array_loader_test.rs +++ b/crates/shirabe/tests/package/loader/validating_array_loader_test.rs @@ -367,7 +367,6 @@ fn success_provider() -> Vec<IndexMap<String, PhpMixed>> { } /// ref: ValidatingArrayLoaderTest::testLoadSuccess -#[ignore] #[test] fn test_load_success() { for cfg in success_provider() { @@ -795,7 +794,6 @@ fn error_provider() -> Vec<(IndexMap<String, PhpMixed>, Vec<String>)> { } /// ref: ValidatingArrayLoaderTest::testLoadFailureThrowsException -#[ignore] #[test] fn test_load_failure_throws_exception() { for (cfg, mut expected_errors) in error_provider() { @@ -977,7 +975,7 @@ fn warning_provider() -> Vec<( } /// ref: ValidatingArrayLoaderTest::testLoadWarnings -#[ignore] +#[ignore = "license warning cases need the SPDX license-expression grammar (recursive PCRE), not yet ported: spdx_licenses todo!()"] #[test] fn test_load_warnings() { for (cfg, mut expected_warnings, _must_check, _expected_array) in warning_provider() { @@ -999,7 +997,7 @@ fn test_load_warnings() { } /// ref: ValidatingArrayLoaderTest::testLoadSkipsWarningDataWhenIgnoringErrors -#[ignore] +#[ignore = "must_check license cases need the SPDX license-expression grammar (recursive PCRE), not yet ported: spdx_licenses todo!()"] #[test] fn test_load_skips_warning_data_when_ignoring_errors() { for (mut cfg, _expected_warnings, must_check, expected_array) in warning_provider() { diff --git a/crates/shirabe/tests/package/root_alias_package_test.rs b/crates/shirabe/tests/package/root_alias_package_test.rs index c275da3..41b1c40 100644 --- a/crates/shirabe/tests/package/root_alias_package_test.rs +++ b/crates/shirabe/tests/package/root_alias_package_test.rs @@ -37,7 +37,6 @@ fn test_update_requires() { } #[test] -#[ignore] fn test_update_dev_requires() { let alias = alias(); assert!(alias.get_dev_requires().is_empty()); @@ -46,7 +45,6 @@ fn test_update_dev_requires() { } #[test] -#[ignore] fn test_update_conflicts() { let alias = alias(); assert!(alias.get_conflicts().is_empty()); @@ -55,7 +53,6 @@ fn test_update_conflicts() { } #[test] -#[ignore] fn test_update_provides() { let alias = alias(); assert!(alias.get_provides().is_empty()); @@ -64,7 +61,6 @@ fn test_update_provides() { } #[test] -#[ignore] fn test_update_replaces() { let alias = alias(); assert!(alias.get_replaces().is_empty()); diff --git a/crates/shirabe/tests/repository/artifact_repository_test.rs b/crates/shirabe/tests/repository/artifact_repository_test.rs index 63fd238..90e5625 100644 --- a/crates/shirabe/tests/repository/artifact_repository_test.rs +++ b/crates/shirabe/tests/repository/artifact_repository_test.rs @@ -35,7 +35,7 @@ fn create_repo(url: &str) -> ArtifactRepository { } #[test] -#[ignore] +#[ignore = "the artifacts fixtures dir contains a .tar file (jsonInRootTarFile); scanning it routes through Tar::get_composer_json -> PharData::new which is todo!()"] fn test_extracts_configs_from_zip_archives() { if set_up() { return; @@ -85,7 +85,7 @@ fn test_extracts_configs_from_zip_archives() { } #[test] -#[ignore] +#[ignore = "the artifacts fixtures dir contains a .tar file (jsonInRootTarFile); scanning it routes through Tar::get_composer_json -> PharData::new which is todo!()"] fn test_absolute_repo_url_creates_absolute_url_packages() { if set_up() { return; @@ -106,7 +106,7 @@ fn test_absolute_repo_url_creates_absolute_url_packages() { } #[test] -#[ignore] +#[ignore = "the relative url is resolved from the process cwd (the crate manifest dir under cargo, not the composer test root), so the artifacts dir is not found; additionally the dir contains a .tar file routing through PharData::new which is todo!()"] fn test_relative_repo_url_creates_relative_url_packages() { if set_up() { return; diff --git a/crates/shirabe/tests/repository/path_repository_test.rs b/crates/shirabe/tests/repository/path_repository_test.rs index 62abbfa..5679c38 100644 --- a/crates/shirabe/tests/repository/path_repository_test.rs +++ b/crates/shirabe/tests/repository/path_repository_test.rs @@ -68,7 +68,7 @@ fn test_load_package_from_file_system_with_version() { ); } -#[ignore] +#[ignore = "version guessing for an unversioned package calls VersionGuesser::guess_git_version, which reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"] #[test] fn test_load_package_from_file_system_without_version() { let repository_url = [ @@ -90,7 +90,7 @@ fn test_load_package_from_file_system_without_version() { assert!(!package_version.is_empty()); } -#[ignore] +#[ignore = "the without-version fixture matched by the wildcard triggers VersionGuesser::guess_git_version, which reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"] #[test] fn test_load_package_from_file_system_with_wildcard() { let repository_url = @@ -160,7 +160,7 @@ fn test_load_package_with_explicit_versions() { } /// Verify relative repository URLs remain relative, see #4439 -#[ignore] +#[ignore = "relies on the process cwd being the test's __DIR__ (the Repository fixtures dir) so the computed relative url resolves; under cargo the cwd is the crate manifest dir, so the relative url does not point at the fixture and getPackages errors"] #[test] fn test_url_remains_relative() { // realpath() does not fully expand the paths @@ -197,7 +197,7 @@ fn test_url_remains_relative() { assert_eq!(Some(relative_url), package.get_dist_url()); } -#[ignore] +#[ignore = "the wildcard url also matches the without-version fixture, whose version guessing reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"] #[test] fn test_reference_none() { let options = coordinates(vec![("reference", PhpMixed::String("none".to_string()))]); @@ -216,7 +216,7 @@ fn test_reference_none() { } } -#[ignore] +#[ignore = "the wildcard url also matches the without-version fixture, whose version guessing reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"] #[test] fn test_reference_config() { let options = coordinates(vec![ diff --git a/crates/shirabe/tests/repository/repository_factory_test.rs b/crates/shirabe/tests/repository/repository_factory_test.rs index c018c45..b1a9001 100644 --- a/crates/shirabe/tests/repository/repository_factory_test.rs +++ b/crates/shirabe/tests/repository/repository_factory_test.rs @@ -12,7 +12,6 @@ use shirabe::util::http_downloader::HttpDownloader; use shirabe_php_shim::PhpMixed; #[test] -#[ignore] fn test_manager_with_all_repository_types() { let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); let config = Rc::new(RefCell::new(Config::new(false, None))); @@ -97,7 +96,6 @@ fn generate_repository_name_provider() -> Vec<( } #[test] -#[ignore] fn test_generate_repository_name() { for (index, repo_pairs, existing_keys, expected) in generate_repository_name_provider() { let repo: IndexMap<String, PhpMixed> = repo_pairs diff --git a/crates/shirabe/tests/repository/repository_manager_test.rs b/crates/shirabe/tests/repository/repository_manager_test.rs index bb85d6a..18f4a7a 100644 --- a/crates/shirabe/tests/repository/repository_manager_test.rs +++ b/crates/shirabe/tests/repository/repository_manager_test.rs @@ -72,7 +72,6 @@ fn str_config(pairs: &[(&str, PhpMixed)]) -> IndexMap<String, PhpMixed> { } #[test] -#[ignore] fn test_prepend() { let SetUp { tmpdir } = set_up(); let _tear_down = TearDown::new(tmpdir.path().to_path_buf()); @@ -90,7 +89,7 @@ fn test_prepend() { } #[test] -#[ignore] +#[ignore = "create_repository routes to RepositoryManager::create_repository_by_class, which is todo!() (dynamic instantiation by class name not yet ported)"] fn test_repo_creation() { let SetUp { tmpdir } = set_up(); let _tear_down = TearDown::new(tmpdir.path().to_path_buf()); @@ -176,7 +175,7 @@ fn test_repo_creation() { } #[test] -#[ignore] +#[ignore = "the first case (\"pear\", a registered type) reaches RepositoryManager::create_repository_by_class, which is todo!() (dynamic instantiation by class name not yet ported), before the \"invalid\" case can be checked"] fn test_invalid_repo_creation_throws() { let SetUp { tmpdir } = set_up(); let _tear_down = TearDown::new(tmpdir.path().to_path_buf()); diff --git a/crates/shirabe/tests/util/forgejo_test.rs b/crates/shirabe/tests/util/forgejo_test.rs index e8bf766..fd6dbc9 100644 --- a/crates/shirabe/tests/util/forgejo_test.rs +++ b/crates/shirabe/tests/util/forgejo_test.rs @@ -1,17 +1,186 @@ //! ref: composer/tests/Composer/Test/Util/ForgejoTest.php -// Both cases construct Forgejo with a mocked IO/Config/JsonConfigSource and a mocked -// HttpDownloader to drive the username/password authentication flow. Mocking is not -// available, and a real HttpDownloader reaches curl_multi_init (todo!()). +use std::cell::RefCell; +use std::rc::Rc; + +use shirabe::config::{Config, ConfigSourceInterface}; +use shirabe::io::IOInterface; +use shirabe::io::io_interface; +use shirabe::util::Forgejo; +use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler}; +use shirabe_php_shim::PhpMixed; + +use crate::config_stub::ConfigStubBuilder; +use crate::http_downloader_mock::{expect_full, get_http_downloader_mock}; +use crate::io_mock::{Expectation, IOMock, get_io_mock}; + +const USERNAME: &str = "username"; +const ACCESS_TOKEN: &str = "access-token"; +const MESSAGE: &str = "mymessage"; +const ORIGIN: &str = "codeberg.org"; + +// Records the config setting names a source has had removed, plus a fixed getName, +// mirroring ForgejoTest's JsonConfigSource mocks (getName -> "auth.json", and the +// config source stubbing removeConfigSetting('forgejo-token.<origin>')). +#[derive(Debug)] +struct ConfigSourceMock { + name: String, + removed: Rc<RefCell<Vec<String>>>, +} + +impl ConfigSourceMock { + fn new(name: &str) -> (Box<Self>, Rc<RefCell<Vec<String>>>) { + let removed = Rc::new(RefCell::new(Vec::new())); + ( + Box::new(Self { + name: name.to_string(), + removed: removed.clone(), + }), + removed, + ) + } +} + +impl ConfigSourceInterface for ConfigSourceMock { + fn add_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _append: bool, + ) -> anyhow::Result<()> { + unreachable!() + } + fn insert_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _reference_name: &str, + _offset: i64, + ) -> anyhow::Result<()> { + unreachable!() + } + fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + Ok(()) + } + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()> { + self.removed.borrow_mut().push(name.to_string()); + Ok(()) + } + fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + unreachable!() + } + fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn add_link(&mut self, _type: &str, _name: &str, _value: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_link(&mut self, _type: &str, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn get_name(&self) -> String { + self.name.clone() + } +} + +fn build_forgejo( + io_mock: &Rc<RefCell<IOMock>>, + config: Rc<RefCell<Config>>, + http_downloader: Rc<RefCell<HttpDownloader>>, +) -> Forgejo { + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + Forgejo::new(io, config, http_downloader) +} #[test] -#[ignore = "requires getIOMock/getHttpDownloaderMock and getMockBuilder mocks of Config/JsonConfigSource with expects()/willReturn(); no mocking infrastructure exists"] fn test_username_password_authentication_flow() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::text(MESSAGE), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Token (hidden): ", ACCESS_TOKEN), + ], + false, + ) + .unwrap(); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![expect_full( + format!("https://{}/api/v1/version", ORIGIN), + None, + 200, + "{}", + vec![], + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + let (auth_source, _) = ConfigSourceMock::new("auth.json"); + let (conf_source, conf_removed) = ConfigSourceMock::new("config.json"); + config.borrow_mut().set_auth_config_source(auth_source); + config.borrow_mut().set_config_source(conf_source); + + let mut forgejo = build_forgejo(&io_mock, config, http_downloader); + + assert!( + forgejo + .authorize_o_auth_interactively(ORIGIN, Some(MESSAGE)) + .unwrap() + .unwrap() + ); + assert_eq!( + *conf_removed.borrow(), + vec![format!("forgejo-token.{}", ORIGIN)] + ); } #[test] -#[ignore = "requires getIOMock/getHttpDownloaderMock and getMockBuilder mocks of Config/JsonConfigSource with expects()/willReturn(); no mocking infrastructure exists"] fn test_username_password_failure() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Token (hidden): ", ACCESS_TOKEN), + ], + false, + ) + .unwrap(); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![expect_full( + format!("https://{}/api/v1/version", ORIGIN), + None, + 404, + "", + vec![], + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + let (auth_source, _) = ConfigSourceMock::new("auth.json"); + config.borrow_mut().set_auth_config_source(auth_source); + + let mut forgejo = build_forgejo(&io_mock, config, http_downloader); + + assert!( + !forgejo + .authorize_o_auth_interactively(ORIGIN, None) + .unwrap() + .unwrap() + ); } diff --git a/crates/shirabe/tests/util/gitlab_test.rs b/crates/shirabe/tests/util/gitlab_test.rs index e7c8bf4..5a91415 100644 --- a/crates/shirabe/tests/util/gitlab_test.rs +++ b/crates/shirabe/tests/util/gitlab_test.rs @@ -1,17 +1,174 @@ //! ref: composer/tests/Composer/Test/Util/GitLabTest.php -// Both cases construct GitLab with a mocked IO/Config/JsonConfigSource and a mocked -// HttpDownloader to drive the username/password authentication flow. Mocking is not -// available, and a real HttpDownloader reaches curl_multi_init (todo!()). +use std::cell::RefCell; +use std::rc::Rc; + +use shirabe::config::{Config, ConfigSourceInterface}; +use shirabe::io::IOInterface; +use shirabe::io::io_interface; +use shirabe::util::GitLab; +use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler}; +use shirabe_php_shim::PhpMixed; + +use crate::config_stub::ConfigStubBuilder; +use crate::http_downloader_mock::{expect_full, get_http_downloader_mock}; +use crate::io_mock::{Expectation, IOMock, get_io_mock}; + +const USERNAME: &str = "username"; +const PASSWORD: &str = "password"; +const MESSAGE: &str = "mymessage"; +const ORIGIN: &str = "gitlab.com"; +const TOKEN: &str = "gitlabtoken"; +const REFRESHTOKEN: &str = "gitlabrefreshtoken"; + +// Mirrors GitLabTest::getAuthJsonMock: a JsonConfigSource whose getName() returns +// "auth.json" and whose addConfigSetting is a no-op (the PHP mock never stubs it). +#[derive(Debug)] +struct AuthJsonMock; + +impl ConfigSourceInterface for AuthJsonMock { + fn add_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _append: bool, + ) -> anyhow::Result<()> { + unreachable!() + } + fn insert_repository( + &mut self, + _name: &str, + _config: PhpMixed, + _reference_name: &str, + _offset: i64, + ) -> anyhow::Result<()> { + unreachable!() + } + fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + Ok(()) + } + fn remove_config_setting(&mut self, _name: &str) -> anyhow::Result<()> { + Ok(()) + } + fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { + unreachable!() + } + fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn add_link(&mut self, _type: &str, _name: &str, _value: &str) -> anyhow::Result<()> { + unreachable!() + } + fn remove_link(&mut self, _type: &str, _name: &str) -> anyhow::Result<()> { + unreachable!() + } + fn get_name(&self) -> String { + "auth.json".to_string() + } +} + +fn set_up(io_mock: &Rc<RefCell<IOMock>>, config: &Rc<RefCell<Config>>) { + config + .borrow_mut() + .set_auth_config_source(Box::new(AuthJsonMock)); + let _ = io_mock; +} -#[ignore = "needs getIOMock/IOMock and getMockBuilder mocks for HttpDownloader/Config/JsonConfigSource (no mock infrastructure)"] #[test] fn test_username_password_authentication_flow() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::text(MESSAGE), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + ], + false, + ) + .unwrap(); + + let body = format!( + "{{\"access_token\": \"{}\", \"refresh_token\": \"{}\", \"token_type\": \"bearer\", \"expires_in\": 7200, \"created_at\": 0}}", + TOKEN, REFRESHTOKEN + ); + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![expect_full( + format!("http://{}/oauth/token", ORIGIN), + None, + 200, + body, + vec![], + )], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + set_up(&io_mock, &config); + + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + let mut gitlab = GitLab::new(io, config, None, Some(http_downloader)).unwrap(); + + assert!( + gitlab + .authorize_oauth_interactively("http", ORIGIN, Some(MESSAGE)) + .unwrap() + ); } -#[ignore = "needs getIOMock/IOMock and getMockBuilder mocks for HttpDownloader/Config/JsonConfigSource (no mock infrastructure)"] #[test] fn test_username_password_failure() { - todo!() + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + Expectation::ask("Username: ", USERNAME), + Expectation::ask("Password: ", PASSWORD), + ], + false, + ) + .unwrap(); + + let (http_downloader, _http_guard) = get_http_downloader_mock( + vec![ + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + expect_full("https://gitlab.com/oauth/token", None, 401, "{}", vec![]), + ], + true, + HttpDownloaderMockHandler::default(), + ); + + let config = ConfigStubBuilder::new().build_shared(); + set_up(&io_mock, &config); + + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + let mut gitlab = GitLab::new(io, config, None, Some(http_downloader)).unwrap(); + + let err = gitlab + .authorize_oauth_interactively("https", ORIGIN, None) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Invalid GitLab credentials 5 times in a row, aborting." + ); } diff --git a/crates/shirabe/tests/util/tls_helper_test.rs b/crates/shirabe/tests/util/tls_helper_test.rs index 4717282..53e39d6 100644 --- a/crates/shirabe/tests/util/tls_helper_test.rs +++ b/crates/shirabe/tests/util/tls_helper_test.rs @@ -1,13 +1,138 @@ //! ref: composer/tests/Composer/Test/Util/TlsHelperTest.php +use indexmap::IndexMap; +use shirabe::util::tls_helper::TlsHelper; +use shirabe_php_shim::PhpMixed; + +// Builds the `['subject' => ['commonName' => ..], 'extensions' => ['subjectAltName' => ..]]` +// certificate array used by the test, given the common name and the subjectAltName string. +fn certificate(common_name: &str, subject_alt_name: &str) -> PhpMixed { + let mut subject = IndexMap::new(); + subject.insert( + "commonName".to_string(), + PhpMixed::String(common_name.to_string()), + ); + let mut extensions = IndexMap::new(); + extensions.insert( + "subjectAltName".to_string(), + PhpMixed::String(subject_alt_name.to_string()), + ); + let mut cert = IndexMap::new(); + cert.insert("subject".to_string(), PhpMixed::Array(subject)); + cert.insert("extensions".to_string(), PhpMixed::Array(extensions)); + PhpMixed::Array(cert) +} + +/// ref: TlsHelperTest::dataCheckCertificateHost +fn data_check_certificate_host() -> Vec<(bool, &'static str, Vec<&'static str>)> { + vec![ + (true, "getcomposer.org", vec!["getcomposer.org"]), + ( + true, + "getcomposer.org", + vec!["getcomposer.org", "packagist.org"], + ), + ( + true, + "getcomposer.org", + vec!["packagist.org", "getcomposer.org"], + ), + (true, "foo.getcomposer.org", vec!["*.getcomposer.org"]), + (false, "xyz.foo.getcomposer.org", vec!["*.getcomposer.org"]), + ( + true, + "foo.getcomposer.org", + vec!["getcomposer.org", "*.getcomposer.org"], + ), + ( + true, + "foo.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + true, + "foo1.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + true, + "foo2.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + false, + "foo2.another.getcomposer.org", + vec!["foo.getcomposer.org", "foo*.getcomposer.org"], + ), + ( + false, + "test.example.net", + vec!["**.example.net", "**.example.net"], + ), + ( + false, + "test.example.net", + vec!["t*t.example.net", "t*t.example.net"], + ), + ( + false, + "xyz.example.org", + vec!["*z.example.org", "*z.example.org"], + ), + ( + false, + "foo.bar.example.com", + vec!["foo.*.example.com", "foo.*.example.com"], + ), + (false, "example.com", vec!["example.*", "example.*"]), + (true, "localhost", vec!["localhost"]), + (false, "localhost", vec!["*"]), + (false, "localhost", vec!["local*"]), + (false, "example.net", vec!["*.net", "*.org", "ex*.net"]), + (true, "example.net", vec!["*.net", "*.org", "example.net"]), + ] +} + #[test] -#[ignore = "TlsHelper::check_certificate_host not implemented in crates/shirabe/src"] fn test_check_certificate_host() { - todo!() + for (expected_result, hostname, mut cert_names) in data_check_certificate_host() { + let expected_cn = cert_names.remove(0); + let subject_alt_name = if cert_names.is_empty() { + String::new() + } else { + format!("DNS:{}", cert_names.join(",DNS:")) + }; + let cert = certificate(expected_cn, &subject_alt_name); + + let mut found_cn: Option<String> = None; + let result = TlsHelper::check_certificate_host(&cert, hostname, &mut found_cn); + + if expected_result { + assert!(result, "hostname {hostname} should match"); + assert_eq!(found_cn.as_deref(), Some(expected_cn)); + } else { + assert!(!result, "hostname {hostname} should not match"); + assert_eq!(found_cn, None); + } + } } #[test] -#[ignore = "TlsHelper::get_certificate_names not implemented in crates/shirabe/src"] fn test_get_certificate_names() { - todo!() + let cert = certificate( + "example.net", + "DNS: example.com, IP: 127.0.0.1, DNS: getcomposer.org, Junk: blah, DNS: composer.example.org", + ); + + let names = TlsHelper::get_certificate_names(&cert).unwrap(); + + assert_eq!(names.cn, "example.net"); + assert_eq!( + names.san, + vec![ + "example.com".to_string(), + "getcomposer.org".to_string(), + "composer.example.org".to_string(), + ] + ); } |
