csaf-core 1.4.11

CSAF storage, validation, sidecar generation, import/export
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Environment-reading half of the self-update policy opt-out.
//!
//! `updater::env_opt_out` is the one function in the updater that consults
//! process state, so the pure suite cannot reach it. Mutation testing flagged
//! exactly that: replacing its body with `false` survived every other test,
//! meaning nothing verified that `CSAF_NO_SELF_UPDATE=1` actually disables
//! self-update. That is the entire purpose of the variable — an operator on a
//! locked-down or package-managed host sets it and expects `--self-update` to
//! refuse.
//!
//! # Why this re-executes itself instead of calling `set_var`
//!
//! Two constraints rule out mutating the environment in-process:
//!
//! 1. The workspace sets `unsafe_code = "deny"`, and `std::env::set_var` is
//!    `unsafe` in edition 2024. Adding a file-scoped `allow` would weaken a
//!    deliberate workspace-wide lint for a test's convenience.
//! 2. `cargo test` runs a binary's tests on multiple THREADS while the
//!    environment is per-PROCESS, so `set_var` is a data race against any
//!    concurrently-running test.
//!
//! Both disappear if the variable is set on a CHILD process instead:
//! `Command::env` / `Command::env_remove` are entirely safe, and the child
//! gets a private environment. So this test re-executes its own test binary
//! once per case, with the variable configured, and asserts on the child's
//! exit status.

use std::process::Command;

use csaf_core::update_cli::UpdateFlags;
use csaf_core::updater::{self, NO_SELF_UPDATE_ENV};

/// Set on the child to switch this test into "probe" mode. Its value is the
/// expectation the child must satisfy: `1` = opted out, `0` = not opted out.
const PROBE_ENV: &str = "CSAF_UPDATER_ENV_PROBE";

/// (`CSAF_NO_SELF_UPDATE` value, must-opt-out). `None` means "unset".
///
/// The falsy rows matter as much as the truthy ones: an operator who writes
/// `=0`, leaves it empty, or typos the value must NOT accidentally disable
/// updates, and someone who writes `=true` must.
const CASES: [(Option<&str>, bool); 17] = [
    (None, false),
    (Some(""), false),
    (Some("0"), false),
    (Some("false"), false),
    (Some("no"), false),
    (Some("off"), false),
    (Some("maybe"), false),
    (Some("2"), false),
    (Some("yes please"), false),
    (Some("1"), true),
    (Some(" 1 "), true),
    (Some("true"), true),
    (Some("TRUE"), true),
    (Some("yes"), true),
    (Some("Yes"), true),
    (Some("on"), true),
    (Some("ON"), true),
];

#[test]
fn env_var_controls_the_self_update_opt_out() {
    if let Ok(expectation) = std::env::var(PROBE_ENV) {
        run_probe(&expectation);
        return;
    }
    let exe = std::env::current_exe().expect("the test binary must have a path");
    for (value, expect_opt_out) in CASES {
        let mut cmd = Command::new(&exe);
        cmd.args(["--exact", "env_var_controls_the_self_update_opt_out"]);
        cmd.env(PROBE_ENV, if expect_opt_out { "1" } else { "0" });
        match value {
            Some(v) => cmd.env(NO_SELF_UPDATE_ENV, v),
            None => cmd.env_remove(NO_SELF_UPDATE_ENV),
        };
        let status = cmd
            .status()
            .expect("re-executing the test binary must work");
        assert!(
            status.success(),
            "{NO_SELF_UPDATE_ENV}={value:?} should give opted_out={expect_opt_out}, \
             but the probe child failed (exit {status})",
        );
    }
}

/// Child side: assert that the inherited environment produces `expectation`.
///
/// Panics — which the parent observes as a non-zero exit status.
fn run_probe(expectation: &str) {
    let want = expectation == "1";
    let raw = std::env::var(NO_SELF_UPDATE_ENV).ok();

    assert_eq!(
        updater::env_opt_out(),
        want,
        "env_opt_out() wrong for {NO_SELF_UPDATE_ENV}={raw:?}",
    );

    // The wrapper the CLI dispatch actually calls must agree, with no flag set.
    assert_eq!(
        UpdateFlags::default().is_opted_out(),
        want,
        "UpdateFlags::is_opted_out() wrong for {NO_SELF_UPDATE_ENV}={raw:?}",
    );

    // An explicit --no-self-update refuses regardless of the environment: the
    // flag is the stronger signal and must never be overridden by it.
    let flagged = UpdateFlags {
        no_self_update: true,
        ..UpdateFlags::default()
    };
    assert!(
        flagged.is_opted_out(),
        "--no-self-update must refuse even with {NO_SELF_UPDATE_ENV}={raw:?}",
    );
}