From 3498bb1ca00ab7d051d296b8d482bea987a00fa4 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 25 Jun 2026 17:02:11 +0900 Subject: test: port 24 command/repository/package/util tests; add TlsHelper Port command (9), util gitlab/forgejo/tls (6), package (6), repository (3) tests. Implement TlsHelper. Fix porting bugs: config_command extra merge, RootAliasPackage setters, ValidatingArrayLoader isset, repository_factory name generation, forgejo exception code, version_parser error chaining. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/shirabe/tests/command/about_command_test.rs | 30 +- crates/shirabe/tests/command/bump_command_test.rs | 5 +- .../command/check_platform_reqs_command_test.rs | 12 +- .../tests/command/clear_cache_command_test.rs | 66 ++- .../shirabe/tests/command/config_command_test.rs | 497 ++++++++++++++++++++- .../shirabe/tests/command/diagnose_command_test.rs | 6 +- crates/shirabe/tests/command/home_command_test.rs | 5 +- .../shirabe/tests/command/validate_command_test.rs | 156 ++++++- 8 files changed, 745 insertions(+), 32 deletions(-) (limited to 'crates/shirabe/tests/command') 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 = 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 = 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 { + // $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: "Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version\nComposer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version\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\nSee https://getcomposer.org/doc/04-schema.md for details on the schema\n# Publish errors\n- name : The property name is required\n- description : The property description is required\n# General warnings\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\nSee https://getcomposer.org/doc/04-schema.md for details on the schema\n# General warnings\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); } -- cgit v1.3.1