mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
* refactor: Update comments for clarity and add expectations.json files for performance metrics * feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks * feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks * refactor: Simplify code formatting for better readability in multiple files * refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration * feat: Update Java and Python patterns to include new security rules * refactor: Improve comment clarity and consistency across multiple Rust files * refactor: Simplify code formatting for improved readability in integration tests and module files * refactor: Improve comment formatting and enhance clarity in assertions across multiple files
284 lines
10 KiB
Rust
284 lines
10 KiB
Rust
//! CLI argument validation regression tests.
|
|
//!
|
|
//! Nyx's surface is a `clap` parser plus a handful of downstream validators
|
|
//! (`SeverityFilter::parse`, `Severity::from_str`, `Confidence::from_str`,
|
|
//! `apply_profile`). These tests lock in the user-visible contract that
|
|
//! bad input exits non-zero with a message that names the offending flag ,
|
|
//! a scanner that silently accepts a typo'd severity and returns zero
|
|
//! findings is a footgun in CI.
|
|
//!
|
|
//! The scanner binary reads its configuration from a platform-dependent
|
|
//! project directory (macOS: `$HOME/Library/Application Support/nyx`;
|
|
//! Linux: `$XDG_CONFIG_HOME/nyx`). Each test redirects both env vars to a
|
|
//! tempdir so the developer's real config is never touched and runs are
|
|
//! reproducible.
|
|
|
|
use assert_cmd::Command;
|
|
use predicates::prelude::*;
|
|
use std::path::PathBuf;
|
|
|
|
/// Build a scan command with a fresh config dir and a writable tempdir as
|
|
/// the scan target. The caller layers extra args on top.
|
|
fn scan_cmd(tmp_home: &std::path::Path, scan_target: &std::path::Path) -> (Command, PathBuf) {
|
|
let mut cmd = Command::cargo_bin("nyx").expect("nyx binary must exist");
|
|
cmd.env("HOME", tmp_home)
|
|
.env("XDG_CONFIG_HOME", tmp_home.join(".config"))
|
|
.env("XDG_DATA_HOME", tmp_home.join(".local/share"))
|
|
// Avoid the welcome banner / animation from interfering with exit codes.
|
|
.env("NO_COLOR", "1");
|
|
cmd.arg("scan").arg(scan_target);
|
|
(cmd, scan_target.to_path_buf())
|
|
}
|
|
|
|
/// Prepare a scan tempdir with a single clean file so the scanner has a
|
|
/// valid target and only the flag being tested should produce an error.
|
|
fn prepare_scan_target() -> tempfile::TempDir {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
std::fs::write(dir.path().join("ok.js"), b"var x = 1;\n").unwrap();
|
|
dir
|
|
}
|
|
|
|
/// Nonexistent scan path: `Path::new(path).canonicalize()?` in `scan::handle`
|
|
/// returns an io::Error, which NyxError wraps and the process exits non-zero.
|
|
#[test]
|
|
fn scan_with_nonexistent_path_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let fake = home.path().join("does/not/exist/anywhere");
|
|
let (mut cmd, _) = scan_cmd(home.path(), &fake);
|
|
|
|
cmd.assert().failure().stderr(
|
|
predicate::str::contains(fake.to_string_lossy().as_ref()).or(
|
|
// On some platforms the error wraps the path inside an IO error
|
|
// message; accept either direct mention or a canonicalize-shaped
|
|
// error so the assertion isn't brittle to errno text. Windows
|
|
// reports ERROR_PATH_NOT_FOUND as "cannot find the path specified".
|
|
predicate::str::contains("canonicalize")
|
|
.or(predicate::str::contains("No such file"))
|
|
.or(predicate::str::contains("not found"))
|
|
.or(predicate::str::contains("cannot find")),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Clap enforces `ValueEnum` for `--format`; an unknown value fails at parse
|
|
/// time with a usage message that lists the valid enum values.
|
|
#[test]
|
|
fn scan_with_unknown_format_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--format").arg("unknown-format-xyz");
|
|
|
|
cmd.assert().failure().stderr(
|
|
predicate::str::contains("format").and(
|
|
predicate::str::contains("unknown-format-xyz")
|
|
.or(predicate::str::contains("possible values")
|
|
.or(predicate::str::contains("invalid value"))),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Clap enforces `ValueEnum` for `--mode`; an unknown value fails at parse
|
|
/// time.
|
|
#[test]
|
|
fn scan_with_unknown_mode_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--mode").arg("bogus-mode-xyz");
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(
|
|
predicate::str::contains("mode").and(
|
|
predicate::str::contains("bogus-mode-xyz")
|
|
.or(predicate::str::contains("invalid value")),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// `--severity BOGUS` fails at `SeverityFilter::parse` with a message naming
|
|
/// the flag.
|
|
#[test]
|
|
fn scan_with_invalid_severity_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--severity").arg("BOGUSSEV");
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("severity"));
|
|
}
|
|
|
|
/// `--fail-on BOGUS` fails at `Severity::from_str`.
|
|
#[test]
|
|
fn scan_with_invalid_fail_on_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--fail-on").arg("BOGUSSEV");
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("fail-on").or(predicate::str::contains("severity")));
|
|
}
|
|
|
|
/// `--min-confidence bogus` fails at `Confidence::from_str`.
|
|
#[test]
|
|
fn scan_with_invalid_min_confidence_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--min-confidence").arg("ultra-extreme");
|
|
|
|
cmd.assert().failure().stderr(
|
|
predicate::str::contains("min-confidence").or(predicate::str::contains("confidence")),
|
|
);
|
|
}
|
|
|
|
/// `--profile nonexistent-profile` fails at `config.apply_profile` which
|
|
/// errors with "unknown profile".
|
|
#[test]
|
|
fn scan_with_unknown_profile_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--profile").arg("not-a-real-profile-xyz");
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(predicate::str::contains("profile"));
|
|
}
|
|
|
|
/// Sanity check: the scan command with no flags on a valid target succeeds.
|
|
/// Guards against a regression where the redirected `HOME` / `XDG_CONFIG_HOME`
|
|
/// setup breaks scans (which would invalidate every negative test above).
|
|
#[test]
|
|
fn scan_with_no_extra_flags_on_clean_target_succeeds() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--format").arg("json");
|
|
|
|
cmd.assert().success();
|
|
}
|
|
|
|
/// `--explain-engine` short-circuits the scan path and prints the resolved
|
|
/// engine configuration to stdout. Exit code 0, non-empty stdout, and the
|
|
/// "Effective engine configuration" header present.
|
|
#[test]
|
|
fn scan_with_explain_engine_prints_config_and_exits_zero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--explain-engine");
|
|
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("Effective engine configuration"))
|
|
.stdout(predicate::str::contains("Abstract interpretation"))
|
|
.stdout(predicate::str::contains("Parse timeout"));
|
|
}
|
|
|
|
/// `--engine-profile` is a `ValueEnum`; valid values parse, invalid values
|
|
/// fail at the clap layer.
|
|
#[test]
|
|
fn scan_with_valid_engine_profile_succeeds() {
|
|
for prof in &["fast", "balanced", "deep"] {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--engine-profile").arg(prof);
|
|
cmd.arg("--explain-engine");
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains(*prof));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scan_with_unknown_engine_profile_exits_nonzero() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--engine-profile").arg("bogus-profile-xyz");
|
|
|
|
cmd.assert()
|
|
.failure()
|
|
.stderr(
|
|
predicate::str::contains("engine-profile").and(
|
|
predicate::str::contains("possible values")
|
|
.or(predicate::str::contains("invalid value")),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Engine-profile + individual flag layering: `--engine-profile fast` turns
|
|
/// backwards analysis off, but a later `--backwards-analysis` flag wins.
|
|
#[test]
|
|
fn scan_engine_profile_is_overridden_by_individual_flag() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let target = prepare_scan_target();
|
|
let (mut cmd, _) = scan_cmd(home.path(), target.path());
|
|
cmd.arg("--engine-profile").arg("fast");
|
|
cmd.arg("--backwards-analysis");
|
|
cmd.arg("--explain-engine");
|
|
|
|
cmd.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("Backwards taint: on"));
|
|
}
|
|
|
|
/// Scanning a directory that contains a C file emits the Preview-tier
|
|
/// banner on stderr. Banner text is asserted loosely to tolerate future
|
|
/// wording changes without going brittle on the exact letter-for-letter
|
|
/// string.
|
|
#[test]
|
|
fn scan_c_file_emits_preview_tier_banner() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
std::fs::write(
|
|
dir.path().join("hello.c"),
|
|
b"#include <stdio.h>\nint main(void) { puts(\"hi\"); return 0; }\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let (mut cmd, _) = scan_cmd(home.path(), dir.path());
|
|
cmd.assert()
|
|
.success()
|
|
.stderr(predicate::str::contains("Preview for C/C++").and(
|
|
predicate::str::contains("Pointer aliasing").or(predicate::str::contains("clang-tidy")),
|
|
));
|
|
}
|
|
|
|
/// `--quiet` must suppress the Preview-tier banner along with the rest of
|
|
/// the status output. Separate test so a regression in quiet-handling
|
|
/// surfaces clearly.
|
|
#[test]
|
|
fn scan_quiet_suppresses_preview_banner() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
std::fs::write(dir.path().join("hello.c"), b"int main(void){return 0;}\n").unwrap();
|
|
|
|
let (mut cmd, _) = scan_cmd(home.path(), dir.path());
|
|
cmd.arg("--quiet");
|
|
cmd.assert()
|
|
.success()
|
|
.stderr(predicate::str::contains("Preview for C/C++").not());
|
|
}
|
|
|
|
/// JSON output format must not print the Preview banner either, machine-
|
|
/// readable output has to stay clean on both stdout and stderr.
|
|
#[test]
|
|
fn scan_json_format_suppresses_preview_banner() {
|
|
let home = tempfile::tempdir().unwrap();
|
|
let dir = tempfile::tempdir().unwrap();
|
|
std::fs::write(dir.path().join("hello.c"), b"int main(void){return 0;}\n").unwrap();
|
|
|
|
let (mut cmd, _) = scan_cmd(home.path(), dir.path());
|
|
cmd.arg("--format").arg("json");
|
|
cmd.assert()
|
|
.success()
|
|
.stderr(predicate::str::contains("Preview for C/C++").not());
|
|
}
|