envseal 0.3.12

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! Mock GUI provider for testing — intercepts approval/passphrase prompts
//! and returns preset responses instead of spawning real dialogs.
//!
//! Activate via the `mock-gui` feature. The mock uses **thread-local**
//! storage so concurrent tests each have isolated state. A mock set on
//! one test thread is invisible to another.

use std::cell::RefCell;
use zeroize::Zeroizing;

use crate::error::Error;
use crate::gui::{Approval, PreexecChoice};

/// Preset responses for the approval dialog.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MockApprovalResponse {
    /// Maps to [`Approval::AllowOnce`].
    AllowOnce,
    /// Maps to [`Approval::AllowAlways`].
    AllowAlways,
    /// Maps to [`Error::UserDenied`].
    Deny,
    /// Also maps to [`Error::UserDenied`] (represents a timeout / no-response).
    Timeout,
}

impl MockApprovalResponse {
    fn into_result(self) -> Result<Approval, Error> {
        match self {
            Self::AllowOnce => Ok(Approval::AllowOnce),
            Self::AllowAlways => Ok(Approval::AllowAlways),
            Self::Deny | Self::Timeout => Err(Error::UserDenied),
        }
    }
}

#[derive(Clone, Debug)]
enum MockState {
    Approval(MockApprovalResponse),
    Passphrase(Zeroizing<String>),
    SecretValue(Zeroizing<String>),
    Totp(String),
    Fido2Pin(Zeroizing<String>),
    Preexec(PreexecChoice),
}

thread_local! {
    static MOCK_STATE: RefCell<Option<MockState>> = const { RefCell::new(None) };
}

/// Set the mock response for approval requests on the current thread.
///
/// The response is reused on every read until [`clear_mock`] is called,
/// so multi-secret inject pipelines that call `request_approval` N times
/// all receive the same preset.
pub fn set_mock_approval(response: MockApprovalResponse) {
    MOCK_STATE.with(|s| *s.borrow_mut() = Some(MockState::Approval(response)));
}

/// Set the mock response for passphrase requests on the current thread.
pub fn set_mock_passphrase(passphrase: &str) {
    MOCK_STATE.with(|s| {
        *s.borrow_mut() = Some(MockState::Passphrase(Zeroizing::new(
            passphrase.to_string(),
        )));
    });
}

/// Set the mock response for secret-value requests on the current thread.
pub fn set_mock_secret_value(value: &str) {
    MOCK_STATE.with(|s| {
        *s.borrow_mut() = Some(MockState::SecretValue(Zeroizing::new(value.to_string())));
    });
}

/// Set the mock response for TOTP requests on the current thread.
pub fn set_mock_totp(code: &str) {
    MOCK_STATE.with(|s| *s.borrow_mut() = Some(MockState::Totp(code.to_string())));
}

/// Set the mock response for FIDO2 PIN requests on the current thread.
pub fn set_mock_fido2_pin(pin: &str) {
    MOCK_STATE.with(|s| {
        *s.borrow_mut() = Some(MockState::Fido2Pin(Zeroizing::new(pin.to_string())));
    });
}

/// Set the mock response for preexec capture prompts on the current thread.
pub fn set_mock_preexec_choice(choice: PreexecChoice) {
    MOCK_STATE.with(|s| *s.borrow_mut() = Some(MockState::Preexec(choice)));
}

/// Clear any active mock response on the current thread.
pub fn clear_mock() {
    MOCK_STATE.with(|s| *s.borrow_mut() = None);
}

pub(crate) fn get_mock_approval() -> Option<Result<Approval, Error>> {
    MOCK_STATE.with(|s| match s.borrow().as_ref() {
        Some(MockState::Approval(r)) => Some(r.into_result()),
        _ => None,
    })
}

pub(crate) fn get_mock_passphrase() -> Option<Zeroizing<String>> {
    MOCK_STATE.with(|s| match s.borrow().as_ref() {
        Some(MockState::Passphrase(r)) => Some(r.clone()),
        _ => None,
    })
}

pub(crate) fn get_mock_secret_value() -> Option<Zeroizing<String>> {
    MOCK_STATE.with(|s| match s.borrow().as_ref() {
        Some(MockState::SecretValue(r)) => Some(r.clone()),
        _ => None,
    })
}

pub(crate) fn get_mock_totp() -> Option<String> {
    MOCK_STATE.with(|s| match s.borrow().as_ref() {
        Some(MockState::Totp(r)) => Some(r.clone()),
        _ => None,
    })
}

pub(crate) fn get_mock_fido2_pin() -> Option<Zeroizing<String>> {
    MOCK_STATE.with(|s| match s.borrow().as_ref() {
        Some(MockState::Fido2Pin(r)) => Some(r.clone()),
        _ => None,
    })
}

pub(crate) fn get_mock_preexec() -> Option<PreexecChoice> {
    MOCK_STATE.with(|s| match s.borrow().as_ref() {
        Some(MockState::Preexec(r)) => Some(*r),
        _ => None,
    })
}