diff --git a/buildwin.bat b/buildwin.bat index ded614e..bd32e19 100644 --- a/buildwin.bat +++ b/buildwin.bat @@ -124,7 +124,9 @@ cargo build --release --target x86_64-pc-windows-msvc || ( echo Generating CHECKSUM.txt... powershell -Command ^ - "Get-FileHash .\target\x86_64-pc-windows-msvc\release\%PROJECT_NAME%.exe -Algorithm SHA256 | Out-File .\target\x86_64-pc-windows-msvc\release\CHECKSUM.txt" + "$hash = Get-FileHash '.\target\x86_64-pc-windows-msvc\release\%PROJECT_NAME%.exe' -Algorithm SHA256;" ^ + "$line = '{0} {1}' -f $hash.Hash, (Split-Path -Leaf $hash.Path);" ^ + "Set-Content -Path '.\target\x86_64-pc-windows-msvc\release\CHECKSUM.txt' -Value $line" if not exist "target\release" mkdir "target\release" copy /Y "target\x86_64-pc-windows-msvc\release\%PROJECT_NAME%.exe" "target\release\" >nul @@ -137,7 +139,10 @@ powershell -Command "Compress-Archive -Path '%PROJECT_NAME%.exe','CHECKSUM-windo if exist "%PROJECT_NAME%-windows-x64.zip" ( REM -- append the ZIP’s SHA-256 to the existing checksum file ---- - certutil -hashfile "%PROJECT_NAME%-windows-x64.zip" SHA256 >> "CHECKSUM-windows-x64.txt" + powershell -Command ^ + "$hash = Get-FileHash '.\%PROJECT_NAME%-windows-x64.zip' -Algorithm SHA256;" ^ + "$line = '{0} {1}' -f $hash.Hash, (Split-Path -Leaf $hash.Path);" ^ + "Add-Content -Path '.\CHECKSUM-windows-x64.txt' -Value $line" echo Created: %PROJECT_NAME%-windows-x64.zip ) else ( echo ERROR: Archive not created. diff --git a/data/rules/anthropic.yml b/data/rules/anthropic.yml index 0bf11fa..4f51c88 100644 --- a/data/rules/anthropic.yml +++ b/data/rules/anthropic.yml @@ -46,5 +46,5 @@ rules: - report_response: true - type: WordMatch words: - - '"type":"invalid_request_error"' + - '"type":"message"' url: https://api.anthropic.com/v1/messages \ No newline at end of file diff --git a/data/rules/microsoftteamswebhook.yml b/data/rules/microsoftteamswebhook.yml index 82fbb19..4da1c1d 100644 --- a/data/rules/microsoftteamswebhook.yml +++ b/data/rules/microsoftteamswebhook.yml @@ -22,15 +22,14 @@ rules: type: Http content: request: - body: | - {'text':''} + body: '{"text":""}' headers: Content-Type: application/json method: POST response_matcher: - type: StatusMatch status: - - 200 + - 400 - report_response: true type: WordMatch words: diff --git a/src/cli/commands/azure.rs b/src/cli/commands/azure.rs index 28e240e..c283d73 100644 --- a/src/cli/commands/azure.rs +++ b/src/cli/commands/azure.rs @@ -47,7 +47,7 @@ pub struct AzureRepoSpecifiers { pub project: Vec, /// Include repositories from all projects within the specified organizations - #[arg(long = "azure-all-projects", alias = "all-azure-projects")] + #[arg(long = "all-projects", alias = "azure-all-projects")] pub all_projects: bool, /// Skip repositories when enumerating Azure sources (format: ORGANIZATION/PROJECT/REPOSITORY) @@ -59,7 +59,7 @@ pub struct AzureRepoSpecifiers { pub exclude_repos: Vec, /// Filter by repository type - #[arg(long = "azure-repo-type", default_value_t = AzureRepoType::Source)] + #[arg(long = "repo-type", alias = "azure-repo-type", default_value_t = AzureRepoType::Source)] pub repo_type: AzureRepoType, } diff --git a/tests/cli_subcommands.rs b/tests/cli_subcommands.rs new file mode 100644 index 0000000..f7af898 --- /dev/null +++ b/tests/cli_subcommands.rs @@ -0,0 +1,1094 @@ +// tests/cli_subcommands.rs +// +// Extensive CLI tests for the new subcommand syntax (kingfisher scan ) +// These tests validate that all platform-specific scan subcommands are properly wired up +// and can be invoked with various argument combinations. + +use assert_cmd::Command; +use predicates::{prelude::PredicateBooleanExt, str::contains}; + +// ============================================================================= +// GitHub Scan Subcommand Tests +// ============================================================================= + +mod github { + use super::*; + + #[test] + fn scan_github_help() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "github", "--help"]) + .assert() + .success() + .stdout(contains("Enumerate and scan GitHub repositories")); + } + + #[test] + fn scan_github_list_only_help() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "github", "--help"]) + .assert() + .success() + .stdout( + contains("--list-only") + .and(contains("List matching repositories without scanning")), + ); + } + + #[test] + fn scan_github_requires_specifier() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "github", "--no-update-check"]) + .assert() + .failure() + .stderr(contains("must specify").or(contains("required")).or(contains("provide"))); + } + + #[test] + fn scan_github_with_user() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + // May succeed (exit 0) or fail with network/auth error (exit 1) + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_with_organization() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--organization", + "testorg", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_multiple_users() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "user1", + "--user", + "user2", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_with_exclude() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "testuser", + "--github-exclude", + "testuser/excluded-repo", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_with_repo_type_fork() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "testuser", + "--repo-type", + "fork", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_with_repo_type_source() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "testuser", + "--repo-type", + "source", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_custom_api_url() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--api-url", + "https://github.enterprise.example.com/api/v3/", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_all_organizations() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--api-url", + "https://github.enterprise.example.com/api/v3/", + "--all-organizations", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_github_invalid_repo_type() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "testuser", + "--repo-type", + "invalid-type", + "--no-update-check", + ]) + .assert() + .failure() + .stderr(contains("invalid value").or(contains("isn't a valid value"))); + } + + #[test] + fn scan_github_mixed_user_and_org() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "testuser", + "--organization", + "testorg", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} + +// ============================================================================= +// GitLab Scan Subcommand Tests +// ============================================================================= + +mod gitlab { + use super::*; + + #[test] + fn scan_gitlab_help() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "gitlab", "--help"]) + .assert() + .success() + .stdout(contains("Enumerate and scan GitLab")); + } + + #[test] + fn scan_gitlab_list_only_flag() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "gitlab", "--help"]) + .assert() + .success() + .stdout(contains("--list-only")); + } + + #[test] + fn scan_gitlab_requires_specifier() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "gitlab", "--no-update-check"]) + .assert() + .failure() + .stderr(contains("must specify").or(contains("required")).or(contains("provide"))); + } + + #[test] + fn scan_gitlab_with_user() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitlab", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitlab_with_group() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitlab", + "--group", + "testgroup", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitlab_with_include_subgroups() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitlab", + "--group", + "testgroup", + "--include-subgroups", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitlab_with_repo_type() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitlab", + "--user", + "testuser", + "--repo-type", + "owner", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitlab_with_exclude() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitlab", + "--user", + "testuser", + "--gitlab-exclude", + "testuser/excluded-repo", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitlab_custom_api_url() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitlab", + "--api-url", + "https://gitlab.enterprise.example.com/", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitlab_all_groups() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitlab", + "--api-url", + "https://gitlab.enterprise.example.com/", + "--all-groups", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} + +// ============================================================================= +// Azure DevOps Scan Subcommand Tests +// ============================================================================= + +mod azure { + use super::*; + + #[test] + fn scan_azure_help() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "azure", "--help"]) + .assert() + .success() + .stdout(contains("Enumerate and scan Azure").or(contains("Azure DevOps"))); + } + + #[test] + fn scan_azure_list_only_flag() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "azure", "--help"]) + .assert() + .success() + .stdout(contains("--list-only")); + } + + #[test] + fn scan_azure_requires_specifier() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "azure", "--no-update-check"]) + .assert() + .failure() + .stderr(contains("must specify").or(contains("required")).or(contains("provide"))); + } + + #[test] + fn scan_azure_with_organization() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "azure", + "--organization", + "testorg", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_azure_with_project() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "azure", + "--project", + "testorg/testproject", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_azure_all_projects() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "azure", + "--organization", + "testorg", + "--all-projects", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_azure_with_exclude() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "azure", + "--organization", + "testorg", + "--azure-exclude", + "testorg/testproject/testrepo", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_azure_with_repo_type() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "azure", + "--organization", + "testorg", + "--repo-type", + "fork", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} + +// ============================================================================= +// Bitbucket Scan Subcommand Tests +// ============================================================================= + +mod bitbucket { + use super::*; + + #[test] + fn scan_bitbucket_help() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "bitbucket", "--help"]) + .assert() + .stdout(contains("kingfisher scan bitbucket [OPTIONS]")); + } + + #[test] + fn scan_bitbucket_list_only_flag() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "bitbucket", "--help"]) + .assert() + .success() + .stdout(contains("--list-only")); + } + + #[test] + fn scan_bitbucket_requires_specifier() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "bitbucket", "--no-update-check"]) + .assert() + .failure() + .stderr(contains("must specify").or(contains("required")).or(contains("provide"))); + } + + #[test] + fn scan_bitbucket_with_workspace() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "bitbucket", + "--workspace", + "testworkspace", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_bitbucket_with_user() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "bitbucket", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_bitbucket_with_project() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "bitbucket", + "--project", + "testproject", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_bitbucket_all_workspaces() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "bitbucket", + "--api-url", + "https://bitbucket.enterprise.example.com/", + "--all-workspaces", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_bitbucket_with_exclude() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "bitbucket", + "--workspace", + "testworkspace", + "--bitbucket-exclude", + "testworkspace/testrepo", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_bitbucket_with_repo_type() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "bitbucket", + "--workspace", + "testworkspace", + "--repo-type", + "source", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} + +// ============================================================================= +// Gitea Scan Subcommand Tests +// ============================================================================= + +mod gitea { + use super::*; + + #[test] + fn scan_gitea_help() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "gitea", "--help"]) + .assert() + .success() + .stdout(contains("Enumerate and scan Gitea")); + } + + #[test] + fn scan_gitea_requires_specifier() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "gitea", "--no-update-check"]) + .assert() + .failure() + .stderr(contains("Specify at least").or(contains("required")).or(contains("provide"))); + } + + #[test] + fn scan_gitea_with_organization() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitea", + "--gitea-organization", + "testorg", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitea_with_user() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitea", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitea_all_organizations() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitea", + "--all-gitea-organizations", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitea_with_exclude() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitea", + "--user", + "testuser", + "--gitea-exclude", + "testuser/testrepo", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitea_with_repo_type() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitea", + "--user", + "testuser", + "--repo-type", + "all", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_gitea_custom_api_url() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "gitea", + "--api-url", + "https://gitea.example.com/api/v1/", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} + +// ============================================================================= +// Hugging Face Scan Subcommand Tests +// ============================================================================= + +mod huggingface { + use super::*; + + #[test] + fn scan_huggingface_help() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "huggingface", "--help"]) + .assert() + .success() + .stdout(contains("Hugging Face").or(contains("HuggingFace"))); + } + + #[test] + fn scan_huggingface_requires_specifier() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "huggingface", "--no-update-check"]) + .assert() + .failure() + .stderr(contains("must specify").or(contains("required")).or(contains("provide"))); + } + + #[test] + fn scan_huggingface_with_user() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "huggingface", + "--huggingface-user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_huggingface_with_organization() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "huggingface", + "--huggingface-organization", + "testorg", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_huggingface_with_model() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "huggingface", + "--huggingface-model", + "testorg/testmodel", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_huggingface_with_dataset() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "huggingface", + "--huggingface-dataset", + "testorg/testdataset", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_huggingface_with_space() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "huggingface", + "--huggingface-space", + "testorg/testspace", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + + #[test] + fn scan_huggingface_with_exclude() { + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "huggingface", + "--huggingface-user", + "testuser", + "--huggingface-exclude", + "testuser/excluded", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} + +// ============================================================================= +// Cross-Platform Tests +// ============================================================================= + +mod cross_platform { + use super::*; + + #[test] + fn all_platforms_support_list_only() { + let platforms = vec![ + vec!["scan", "github", "--user", "test"], + vec!["scan", "gitlab", "--user", "test"], + vec!["scan", "azure", "--organization", "test"], + vec!["scan", "bitbucket", "--workspace", "test"], + vec!["scan", "gitea", "--user", "test"], + vec!["scan", "huggingface", "--huggingface-user", "test"], + ]; + + for mut platform_args in platforms { + platform_args.extend_from_slice(&["--list-only", "--no-update-check"]); + Command::cargo_bin("kingfisher") + .unwrap() + .args(&platform_args) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } + } + + #[test] + fn scan_requires_subcommand_or_path() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "--no-update-check"]) + .assert() + .failure() + .stderr( + contains("required") + .or(contains("expected")) + .or(contains("provide")), + ); + } + + #[test] + fn scan_invalid_platform_subcommand() { + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", "invalid-platform", "--no-update-check"]) + .assert() + .failure() + .stderr(contains("unrecognized").or(contains("unexpected"))); + } + + #[test] + fn scan_github_without_scanning_no_paths() { + // list-only should work without providing actual scan paths + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "testuser", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} + +// ============================================================================= +// Legacy vs New Syntax Coexistence Tests +// ============================================================================= + +mod legacy_compatibility { + use super::*; + + #[test] + fn scan_path_still_works() { + // The old syntax of scanning a local path should still work + Command::cargo_bin("kingfisher") + .unwrap() + .args(["scan", ".", "--no-validate", "--no-update-check"]) + .assert() + .code(predicates::function::function(|code: &i32| { + // May succeed or fail depending on rules, but shouldn't error on syntax + *code == 0 || *code == 1 || *code == 200 + })); + } + + #[test] + fn new_subcommand_syntax_coexists() { + // Verify both old and new work (even if they fail due to network/auth) + // This just validates the CLI parsing works + + // New syntax + Command::cargo_bin("kingfisher") + .unwrap() + .args([ + "scan", + "github", + "--user", + "test", + "--list-only", + "--no-update-check", + ]) + .assert() + .code(predicates::function::function(|code: &i32| { + *code == 0 || *code == 1 + })); + } +} \ No newline at end of file