csaf-crud 1.3.7

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Regression test for `csaf-crud --help` / `-h` / `--version` / `-V`.
//!
//! Before this fix, `main()` treated `std::env::args().nth(1)` unconditionally
//! as a config-file path, so `csaf-crud --help` silently started the full TLS
//! server (binding sockets, spawning listeners) instead of printing usage and
//! exiting. That hung indefinitely under a release smoke test. Every case
//! here bounds the subprocess with a timeout so a regression to that
//! behaviour fails the test instead of hanging the test runner.

// Integration test files are their own compilation unit, separate from
// crates/csaf-crud/src/main.rs -- that binary's `#![cfg_attr(test, allow(...))]`
// (if any) doesn't cover them, so declare the standard test-only relaxation
// here. Production code remains strict.
#![allow(clippy::expect_used)]

use std::io::Read as _;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;

/// Run the compiled `csaf-crud` binary with `arg`, returning `(exit code,
/// stdout)` if it exits within 30 seconds, or `None` if it does not (the
/// regression this test guards against). 30s gives ample headroom over
/// process-spawn latency under concurrent CI/build load while staying
/// nowhere near the original bug's multi-minute hang.
fn run_with_timeout(arg: &str) -> Option<(i32, String)> {
    let mut child = Command::new(env!("CARGO_BIN_EXE_csaf-crud"))
        .arg(arg)
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .expect("failed to spawn csaf-crud");

    let (tx, rx) = mpsc::channel();
    std::thread::spawn(move || {
        let status = child.wait();
        let mut stdout = String::new();
        if let Some(mut out) = child.stdout.take() {
            let _ = out.read_to_string(&mut stdout);
        }
        let _ = tx.send((status, stdout));
    });

    match rx.recv_timeout(Duration::from_secs(30)) {
        Ok((Ok(status), stdout)) => Some((status.code().unwrap_or(-1), stdout)),
        Ok((Err(_), _)) | Err(_) => None,
    }
}

#[test]
fn help_long_flag_prints_usage_and_exits_promptly() {
    let (code, stdout) = run_with_timeout("--help")
        .expect("csaf-crud --help hung instead of printing usage and exiting");
    assert_eq!(code, 0);
    assert!(stdout.contains("USAGE:"));
    assert!(stdout.contains("csaf-crud"));
}

#[test]
fn help_short_flag_prints_usage_and_exits_promptly() {
    let (code, stdout) =
        run_with_timeout("-h").expect("csaf-crud -h hung instead of printing usage and exiting");
    assert_eq!(code, 0);
    assert!(stdout.contains("USAGE:"));
}

#[test]
fn version_long_flag_prints_version_and_exits_promptly() {
    let (code, stdout) = run_with_timeout("--version")
        .expect("csaf-crud --version hung instead of printing the version and exiting");
    assert_eq!(code, 0);
    assert!(stdout.contains(env!("CARGO_PKG_VERSION")));
}

#[test]
fn version_short_flag_prints_version_and_exits_promptly() {
    let (code, stdout) = run_with_timeout("-V")
        .expect("csaf-crud -V hung instead of printing the version and exiting");
    assert_eq!(code, 0);
    assert!(stdout.contains(env!("CARGO_PKG_VERSION")));
}