Skip to main content

pacsea/logic/
password.rs

1//! Privilege password validation utilities.
2//!
3//! Delegates to [`crate::logic::privilege`] for tool-aware checks.
4
5use crate::logic::privilege::AuthMode;
6
7/// What: Resolve the effective authentication mode from settings.
8///
9/// Inputs:
10/// - `settings`: Reference to the application settings.
11///
12/// Output:
13/// - The resolved [`AuthMode`] to use for privilege escalation.
14///
15/// Details:
16/// - If `auth_mode` is explicitly set to something other than the default (`Prompt`),
17///   it takes precedence.
18/// - If `auth_mode` is `Prompt` (the default) and the legacy `use_passwordless_sudo`
19///   is `true`, maps to `PasswordlessOnly` for backward compatibility and logs a
20///   deprecation warning.
21/// - When both `auth_mode != Prompt` and `use_passwordless_sudo = true` are set,
22///   `auth_mode` wins and a deprecation warning is logged.
23#[must_use]
24pub fn resolve_auth_mode(settings: &crate::theme::Settings) -> AuthMode {
25    static WARNED_LEGACY_PASSWORDLESS_MAPPING: std::sync::Once = std::sync::Once::new();
26    static WARNED_LEGACY_PASSWORDLESS_CONFLICT: std::sync::Once = std::sync::Once::new();
27
28    if crate::logic::privilege::is_integration_test() {
29        if let Ok(val) = std::env::var("PACSEA_TEST_AUTH_MODE") {
30            tracing::debug!(val = %val, "Using test override for resolve_auth_mode");
31            if let Some(mode) = AuthMode::from_config_key(&val) {
32                return coerce_prompt_mode_for_tool_capabilities(mode);
33            }
34        }
35        if std::env::var("PACSEA_TEST_SUDO_PASSWORDLESS")
36            .ok()
37            .as_deref()
38            == Some("1")
39        {
40            tracing::debug!("Legacy test env PACSEA_TEST_SUDO_PASSWORDLESS=1 → PasswordlessOnly");
41            return coerce_prompt_mode_for_tool_capabilities(AuthMode::PasswordlessOnly);
42        }
43    }
44
45    let explicit_auth_mode = settings.auth_mode;
46    let legacy_passwordless = settings.use_passwordless_sudo;
47
48    let resolved = match (explicit_auth_mode, legacy_passwordless) {
49        (AuthMode::Prompt, true) => {
50            WARNED_LEGACY_PASSWORDLESS_MAPPING.call_once(|| {
51                tracing::warn!(
52                    "Deprecated: 'use_passwordless_sudo = true' is active. \
53                     Mapping to auth_mode = passwordless_only. \
54                     Please migrate to 'auth_mode = passwordless_only' in settings.conf."
55                );
56            });
57            AuthMode::PasswordlessOnly
58        }
59        (mode, true) if mode != AuthMode::Prompt => {
60            WARNED_LEGACY_PASSWORDLESS_CONFLICT.call_once(|| {
61                tracing::warn!(
62                    auth_mode = %mode,
63                    "Deprecated: 'use_passwordless_sudo' is set alongside 'auth_mode'. \
64                     'auth_mode = {mode}' takes precedence. \
65                     Please remove 'use_passwordless_sudo' from settings.conf."
66                );
67            });
68            mode
69        }
70        (mode, _) => mode,
71    };
72
73    coerce_prompt_mode_for_tool_capabilities(resolved)
74}
75
76/// What: Ensure auth mode is compatible with the active privilege tool.
77///
78/// Inputs:
79/// - `mode`: Resolved auth mode from settings and legacy compatibility logic.
80///
81/// Output:
82/// - Compatible auth mode for the active privilege tool.
83///
84/// Details:
85/// - If `mode` is `Prompt` and the active tool cannot read passwords from stdin
86///   (e.g. doas), this coerces to `Interactive`.
87/// - This avoids entering in-app password validation paths that can never succeed.
88/// - If the active tool cannot be resolved, returns the original mode so regular
89///   tool-resolution errors can be surfaced by callers later.
90fn coerce_prompt_mode_for_tool_capabilities(mode: AuthMode) -> AuthMode {
91    if mode != AuthMode::Prompt {
92        return mode;
93    }
94
95    let tool = match crate::logic::privilege::active_tool() {
96        Ok(tool) => tool,
97        Err(err) => {
98            tracing::debug!(
99                error = %err,
100                "Could not resolve active privilege tool while resolving auth mode; leaving mode unchanged"
101            );
102            return mode;
103        }
104    };
105
106    if tool.capabilities().supports_stdin_password {
107        return mode;
108    }
109
110    tracing::warn!(
111        tool = %tool,
112        configured_mode = %mode,
113        forced_mode = %AuthMode::Interactive,
114        "Auth mode 'prompt' is incompatible with the active privilege tool; forcing interactive auth"
115    );
116    AuthMode::Interactive
117}
118
119/// What: Determine whether Pacsea should perform interactive auth handoff.
120///
121/// Inputs:
122/// - `settings`: Reference to the application settings.
123///
124/// Output:
125/// - `true` when resolved auth mode is `Interactive`, otherwise `false`.
126///
127/// Details:
128/// - Uses [`resolve_auth_mode`] so capability-based coercions are respected.
129/// - Covers both explicit `auth_mode = interactive` and compatibility fallbacks
130///   (for example, `doas + prompt` coercion).
131#[must_use]
132pub fn should_use_interactive_auth_handoff(settings: &crate::theme::Settings) -> bool {
133    resolve_auth_mode(settings) == AuthMode::Interactive
134}
135
136/// What: Determine whether interactive handoff is being forced by compatibility fallback.
137///
138/// Inputs:
139/// - `settings`: Reference to the application settings.
140///
141/// Output:
142/// - `true` when resolved mode is `Interactive` but configured mode is not explicitly interactive.
143///
144/// Details:
145/// - Useful for diagnostics/UX messaging that differentiates explicit user intent
146///   from forced compatibility behavior.
147#[must_use]
148pub fn should_force_interactive_auth_handoff(settings: &crate::theme::Settings) -> bool {
149    resolve_auth_mode(settings) == AuthMode::Interactive
150        && settings.auth_mode != AuthMode::Interactive
151}
152
153/// What: Determine whether the Pacsea password modal should be skipped.
154///
155/// Inputs:
156/// - `settings`: Reference to the application settings.
157///
158/// Output:
159/// - `true` if the password modal should be skipped, `false` if it should be shown.
160///
161/// Details:
162/// - Resolves the effective [`AuthMode`] via [`resolve_auth_mode`].
163/// - `Interactive` always skips the modal.
164/// - `PasswordlessOnly` skips only when `{tool} -n true` succeeds on the system.
165/// - `Prompt` never skips the modal.
166/// - Tool-agnostic: works identically for sudo and doas.
167#[must_use]
168pub fn should_skip_password_modal(settings: &crate::theme::Settings) -> bool {
169    let mode = resolve_auth_mode(settings);
170    match mode {
171        AuthMode::Interactive => {
172            tracing::info!("Auth mode is 'interactive'; skipping Pacsea password modal");
173            true
174        }
175        AuthMode::PasswordlessOnly => should_use_passwordless_sudo(settings),
176        AuthMode::Prompt => false,
177    }
178}
179
180/// What: Check if passwordless privilege escalation is available for the current user.
181///
182/// Inputs:
183/// - None (uses the active privilege tool from settings).
184///
185/// Output:
186/// - `Ok(true)` if passwordless execution is available, `Ok(false)` if not, or `Err(String)` on error.
187///
188/// # Errors
189///
190/// - Returns `Err` if the check cannot be executed (e.g., tool not installed).
191///
192/// Details:
193/// - Delegates to [`crate::logic::privilege::PrivilegeTool::check_passwordless`].
194/// - Uses the resolved privilege tool (sudo or doas) based on settings.
195/// - Both sudo and doas support `-n true` for non-interactive checking.
196pub fn check_passwordless_sudo_available() -> Result<bool, String> {
197    let tool = crate::logic::privilege::active_tool()?;
198    tool.check_passwordless()
199}
200
201/// What: Check if passwordless privilege escalation should be used based on settings and system availability.
202///
203/// Inputs:
204/// - `settings`: Reference to the application settings.
205///
206/// Output:
207/// - `true` if passwordless execution should be used, `false` otherwise.
208///
209/// Details:
210/// - This function is strictly about passwordless availability (`{tool} -n true`).
211/// - For non-`PasswordlessOnly` modes, checks if `use_passwordless_sudo` is enabled
212///   in settings (legacy safety barrier).
213/// - If legacy toggle is required but disabled, returns `false` immediately.
214/// - If enabled, checks if passwordless execution is actually available on the system.
215/// - Returns `true` only if both conditions are met.
216/// - Tool capability constraints (for example: doas lacking stdin password support) are
217///   handled separately via [`should_use_interactive_auth_handoff`].
218/// - Test overrides flow through [`check_passwordless_sudo_available`] via privilege module.
219#[must_use]
220pub fn should_use_passwordless_sudo(settings: &crate::theme::Settings) -> bool {
221    // In integration test context, honor the test override directly.
222    // This bypasses the settings check so tests can simulate passwordless without
223    // modifying the persisted Settings struct.
224    if crate::logic::privilege::is_integration_test()
225        && let Ok(val) = std::env::var("PACSEA_TEST_SUDO_PASSWORDLESS")
226    {
227        tracing::debug!(
228            val = %val,
229            "Using test override for should_use_passwordless_sudo"
230        );
231        return val == "1";
232    }
233
234    let auth_mode = resolve_auth_mode(settings);
235    let require_legacy_toggle = auth_mode != AuthMode::PasswordlessOnly;
236    if require_legacy_toggle && !settings.use_passwordless_sudo {
237        tracing::debug!("Passwordless privilege disabled in settings, requiring password prompt");
238        return false;
239    }
240
241    match check_passwordless_sudo_available() {
242        Ok(true) => {
243            tracing::info!("Passwordless privilege enabled in settings and available on system");
244            true
245        }
246        Ok(false) => {
247            tracing::debug!(
248                "Passwordless privilege enabled in settings but not available on system, requiring password prompt"
249            );
250            false
251        }
252        Err(e) => {
253            tracing::debug!(
254                "Passwordless privilege check failed ({}), requiring password prompt",
255                e
256            );
257            false
258        }
259    }
260}
261
262/// What: Validate a privilege tool password without executing any command.
263///
264/// Inputs:
265/// - `password`: Password to validate.
266///
267/// Output:
268/// - `Ok(true)` if password is valid, `Ok(false)` if invalid, or `Err(String)` on error.
269///
270/// # Errors
271///
272/// - Returns `Err` if the validation command cannot be executed (e.g., tool not available).
273/// - Returns `Err` if the active tool does not support password validation (e.g., doas).
274///
275/// Details:
276/// - Delegates to [`crate::logic::privilege::validate_password`].
277/// - Only works for tools that support stdin password piping (currently sudo).
278/// - For doas, returns an error since doas cannot validate passwords via stdin.
279pub fn validate_sudo_password(password: &str) -> Result<bool, String> {
280    let tool = crate::logic::privilege::active_tool()?;
281    crate::logic::privilege::validate_password(tool, password)
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    /// What: Check if passwordless sudo is configured (test helper).
289    ///
290    /// Inputs:
291    /// - None.
292    ///
293    /// Output:
294    /// - `true` if passwordless sudo is available, `false` otherwise.
295    ///
296    /// Details:
297    /// - Uses the public `check_passwordless_sudo_available()` function.
298    /// - Returns `false` if sudo is not available or requires a password.
299    fn is_passwordless_sudo() -> bool {
300        check_passwordless_sudo_available().unwrap_or(false)
301    }
302
303    #[test]
304    /// What: Test passwordless sudo check returns a valid result.
305    ///
306    /// Inputs:
307    /// - None.
308    ///
309    /// Output:
310    /// - Returns `Ok(bool)` without panicking.
311    ///
312    /// Details:
313    /// - Verifies the function returns a valid result (either true or false).
314    /// - Does not assert on the actual value since it depends on system configuration.
315    fn test_check_passwordless_sudo_available() {
316        let _guard = crate::global_test_mutex_lock();
317        unsafe {
318            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
319            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
320            std::env::set_var("PACSEA_TEST_SUDO_PASSWORDLESS", "1");
321        }
322
323        let result = check_passwordless_sudo_available();
324
325        unsafe {
326            std::env::remove_var("PACSEA_TEST_SUDO_PASSWORDLESS");
327            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
328            std::env::remove_var("PACSEA_INTEGRATION_TEST");
329        }
330
331        assert_eq!(result, Ok(true));
332    }
333
334    #[test]
335    /// What: Ensure doas no longer implies passwordless execution by capability alone.
336    ///
337    /// Inputs:
338    /// - Integration test env forcing only doas availability.
339    /// - Passwordless override set to disabled.
340    /// - Explicit `auth_mode = passwordless_only`.
341    ///
342    /// Output:
343    /// - Returns `false` from `should_use_passwordless_sudo`.
344    ///
345    /// Details:
346    /// - Ensures `PasswordlessOnly` strictly follows `{tool} -n true` availability.
347    /// - Prevents capability-based auto-pass for doas.
348    fn test_should_use_passwordless_sudo_false_for_doas_when_passwordless_unavailable() {
349        let _guard = crate::global_test_mutex_lock();
350        unsafe {
351            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
352            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "doas");
353            std::env::set_var("PACSEA_TEST_AUTH_MODE", "passwordless_only");
354            std::env::set_var("PACSEA_TEST_SUDO_PASSWORDLESS", "0");
355        }
356
357        let settings = crate::theme::Settings::default();
358        let should_skip_prompt = should_use_passwordless_sudo(&settings);
359
360        unsafe {
361            std::env::remove_var("PACSEA_TEST_SUDO_PASSWORDLESS");
362            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
363            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
364            std::env::remove_var("PACSEA_INTEGRATION_TEST");
365        }
366
367        assert!(
368            !should_skip_prompt,
369            "doas should not be treated as passwordless when -n check fails"
370        );
371    }
372
373    #[test]
374    /// What: Prompt mode with doas forces interactive auth handoff.
375    ///
376    /// Inputs:
377    /// - Integration test env forcing doas availability.
378    /// - Default settings (`auth_mode = prompt`).
379    ///
380    /// Output:
381    /// - Returns `true` from `should_use_interactive_auth_handoff`.
382    ///
383    /// Details:
384    /// - doas cannot support in-app stdin password validation.
385    fn test_should_use_interactive_auth_handoff_true_for_prompt_doas() {
386        let _guard = crate::global_test_mutex_lock();
387        unsafe {
388            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
389            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "doas");
390            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
391        }
392
393        let settings = crate::theme::Settings::default();
394        let result = should_use_interactive_auth_handoff(&settings);
395
396        unsafe {
397            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
398            std::env::remove_var("PACSEA_INTEGRATION_TEST");
399        }
400
401        assert!(
402            result,
403            "doas + prompt should force interactive auth handoff"
404        );
405    }
406
407    // -- resolve_auth_mode ---------------------------------------------------
408
409    #[test]
410    /// What: Default settings resolve to `Prompt` auth mode.
411    ///
412    /// Inputs: Default settings (`auth_mode` = Prompt, `use_passwordless_sudo` = false).
413    ///
414    /// Output: `AuthMode::Prompt`.
415    ///
416    /// Details: Ensures no accidental legacy mapping fires for default config.
417    fn test_resolve_auth_mode_default_is_prompt() {
418        let _guard = crate::global_test_mutex_lock();
419        unsafe {
420            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
421            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
422            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
423        }
424
425        let settings = crate::theme::Settings::default();
426        let mode = resolve_auth_mode(&settings);
427
428        unsafe {
429            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
430            std::env::remove_var("PACSEA_INTEGRATION_TEST");
431        }
432
433        assert_eq!(mode, AuthMode::Prompt);
434    }
435
436    #[test]
437    /// What: Prompt mode is coerced to interactive for doas.
438    ///
439    /// Inputs:
440    /// - Integration test env forcing doas as the only available tool.
441    /// - Default settings (`auth_mode = Prompt`).
442    ///
443    /// Output: `AuthMode::Interactive`.
444    ///
445    /// Details:
446    /// - doas does not support stdin password validation.
447    /// - This ensures prompt-mode password modal flow cannot be reached with doas.
448    fn test_resolve_auth_mode_prompt_for_doas_forces_interactive() {
449        let _guard = crate::global_test_mutex_lock();
450        unsafe {
451            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
452            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "doas");
453            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
454        }
455
456        let settings = crate::theme::Settings::default();
457        let mode = resolve_auth_mode(&settings);
458
459        unsafe {
460            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
461            std::env::remove_var("PACSEA_INTEGRATION_TEST");
462        }
463
464        assert_eq!(mode, AuthMode::Interactive);
465    }
466
467    #[test]
468    /// What: Explicit `auth_mode = interactive` takes effect.
469    ///
470    /// Inputs: Settings with `auth_mode = Interactive`.
471    ///
472    /// Output: `AuthMode::Interactive`.
473    ///
474    /// Details: Verifies direct setting without legacy fallback.
475    fn test_resolve_auth_mode_explicit_interactive() {
476        let _guard = crate::global_test_mutex_lock();
477        unsafe {
478            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
479            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
480            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
481        }
482
483        let settings = crate::theme::Settings {
484            auth_mode: AuthMode::Interactive,
485            ..crate::theme::Settings::default()
486        };
487        let mode = resolve_auth_mode(&settings);
488
489        unsafe {
490            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
491            std::env::remove_var("PACSEA_INTEGRATION_TEST");
492        }
493
494        assert_eq!(mode, AuthMode::Interactive);
495    }
496
497    #[test]
498    /// What: Legacy `use_passwordless_sudo = true` maps to `PasswordlessOnly`.
499    ///
500    /// Inputs: Settings with `auth_mode = Prompt` (default) and `use_passwordless_sudo = true`.
501    ///
502    /// Output: `AuthMode::PasswordlessOnly`.
503    ///
504    /// Details: Backward compatibility mapping fires when `auth_mode` is still default.
505    fn test_resolve_auth_mode_legacy_passwordless_maps() {
506        let _guard = crate::global_test_mutex_lock();
507        unsafe {
508            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
509            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
510            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
511        }
512
513        let settings = crate::theme::Settings {
514            use_passwordless_sudo: true,
515            ..crate::theme::Settings::default()
516        };
517        let mode = resolve_auth_mode(&settings);
518
519        unsafe {
520            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
521            std::env::remove_var("PACSEA_INTEGRATION_TEST");
522        }
523
524        assert_eq!(mode, AuthMode::PasswordlessOnly);
525    }
526
527    #[test]
528    /// What: Explicit `auth_mode = interactive` wins over legacy `use_passwordless_sudo`.
529    ///
530    /// Inputs: `auth_mode = Interactive` and `use_passwordless_sudo = true`.
531    ///
532    /// Output: `AuthMode::Interactive` (explicit `auth_mode` wins).
533    ///
534    /// Details: When both keys are set, `auth_mode` takes precedence over legacy.
535    fn test_resolve_auth_mode_explicit_wins_over_legacy() {
536        let _guard = crate::global_test_mutex_lock();
537        unsafe {
538            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
539            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
540            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
541        }
542
543        let settings = crate::theme::Settings {
544            auth_mode: AuthMode::Interactive,
545            use_passwordless_sudo: true,
546            ..crate::theme::Settings::default()
547        };
548        let mode = resolve_auth_mode(&settings);
549
550        unsafe {
551            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
552            std::env::remove_var("PACSEA_INTEGRATION_TEST");
553        }
554
555        assert_eq!(mode, AuthMode::Interactive);
556    }
557
558    #[test]
559    /// What: Test override env var controls resolved auth mode.
560    ///
561    /// Inputs: `PACSEA_TEST_AUTH_MODE=interactive` with default settings.
562    ///
563    /// Output: `AuthMode::Interactive`.
564    ///
565    /// Details: Integration test override should bypass settings entirely.
566    fn test_resolve_auth_mode_env_override() {
567        let _guard = crate::global_test_mutex_lock();
568        unsafe {
569            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
570            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
571            std::env::set_var("PACSEA_TEST_AUTH_MODE", "interactive");
572        }
573
574        let settings = crate::theme::Settings::default();
575        let mode = resolve_auth_mode(&settings);
576
577        unsafe {
578            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
579            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
580            std::env::remove_var("PACSEA_INTEGRATION_TEST");
581        }
582
583        assert_eq!(mode, AuthMode::Interactive);
584    }
585
586    // -- should_skip_password_modal ------------------------------------------
587
588    #[test]
589    /// What: `should_skip_password_modal` returns true for interactive mode.
590    ///
591    /// Inputs: Settings with `auth_mode = Interactive`.
592    ///
593    /// Output: `true`.
594    ///
595    /// Details: Interactive always skips the modal, regardless of tool availability.
596    fn test_should_skip_password_modal_interactive() {
597        let _guard = crate::global_test_mutex_lock();
598        unsafe {
599            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
600            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
601            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
602        }
603
604        let settings = crate::theme::Settings {
605            auth_mode: AuthMode::Interactive,
606            ..crate::theme::Settings::default()
607        };
608        let skip = should_skip_password_modal(&settings);
609
610        unsafe {
611            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
612            std::env::remove_var("PACSEA_INTEGRATION_TEST");
613        }
614
615        assert!(skip, "Interactive mode should always skip password modal");
616    }
617
618    #[test]
619    /// What: `should_skip_password_modal` returns true for interactive mode with doas.
620    ///
621    /// Inputs: Settings with `auth_mode = Interactive` and only doas available.
622    ///
623    /// Output: `true`.
624    ///
625    /// Details: Interactive mode is tool-agnostic — must skip for doas too.
626    fn test_should_skip_password_modal_interactive_doas() {
627        let _guard = crate::global_test_mutex_lock();
628        unsafe {
629            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
630            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "doas");
631            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
632        }
633
634        let settings = crate::theme::Settings {
635            auth_mode: AuthMode::Interactive,
636            ..crate::theme::Settings::default()
637        };
638        let skip = should_skip_password_modal(&settings);
639
640        unsafe {
641            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
642            std::env::remove_var("PACSEA_INTEGRATION_TEST");
643        }
644
645        assert!(
646            skip,
647            "Interactive mode should skip password modal for doas too"
648        );
649    }
650
651    #[test]
652    /// What: `should_skip_password_modal` returns false for default prompt mode.
653    ///
654    /// Inputs: Default settings.
655    ///
656    /// Output: `false`.
657    ///
658    /// Details: Prompt mode always shows the modal.
659    fn test_should_skip_password_modal_prompt() {
660        let _guard = crate::global_test_mutex_lock();
661        unsafe {
662            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
663            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "sudo");
664            std::env::remove_var("PACSEA_TEST_AUTH_MODE");
665        }
666
667        let settings = crate::theme::Settings::default();
668        let skip = should_skip_password_modal(&settings);
669
670        unsafe {
671            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
672            std::env::remove_var("PACSEA_INTEGRATION_TEST");
673        }
674
675        assert!(!skip, "Prompt mode should always show password modal");
676    }
677
678    #[test]
679    #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
680    /// What: Test password validation handles invalid passwords.
681    ///
682    /// Inputs:
683    /// - Invalid password string.
684    ///
685    /// Output:
686    /// - Returns `Ok(false)` for invalid password.
687    ///
688    /// Details:
689    /// - Verifies the function correctly identifies invalid passwords.
690    /// - Skips assertion if passwordless sudo is configured (common in CI).
691    /// - Marked as ignored to prevent user lockout from failed sudo attempts.
692    fn test_validate_sudo_password_invalid() {
693        // Skip test if passwordless sudo is configured (common in CI environments)
694        if is_passwordless_sudo() {
695            return;
696        }
697
698        // This test uses an obviously wrong password
699        // It should return Ok(false) without panicking
700        let result = validate_sudo_password("definitely_wrong_password_12345");
701        // Result may be Ok(false) or Err depending on system configuration
702        if let Ok(valid) = result {
703            // Should be false for invalid password
704            assert!(!valid);
705        } else {
706            // Error is acceptable (e.g., sudo not available)
707        }
708    }
709
710    #[test]
711    #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
712    /// What: Test password validation handles empty passwords.
713    ///
714    /// Inputs:
715    /// - Empty password string.
716    ///
717    /// Output:
718    /// - Returns `Ok(false)` for empty password.
719    ///
720    /// Details:
721    /// - Verifies the function correctly handles empty passwords.
722    /// - Skips assertion if passwordless sudo is configured (common in CI).
723    /// - Marked as ignored to prevent user lockout from failed sudo attempts.
724    fn test_validate_sudo_password_empty() {
725        // Skip test if passwordless sudo is configured (common in CI environments)
726        if is_passwordless_sudo() {
727            return;
728        }
729
730        let result = validate_sudo_password("");
731        // Empty password should be invalid
732        if let Ok(valid) = result {
733            assert!(!valid);
734        } else {
735            // Error is acceptable
736        }
737    }
738
739    #[test]
740    #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
741    /// What: Test password validation handles special characters.
742    ///
743    /// Inputs:
744    /// - Password with special characters that need escaping.
745    ///
746    /// Output:
747    /// - Handles special characters without panicking.
748    ///
749    /// Details:
750    /// - Verifies the function correctly escapes special characters in passwords.
751    /// - Marked as ignored to prevent user lockout from failed sudo attempts.
752    fn test_validate_sudo_password_special_chars() {
753        // Test with password containing special shell characters
754        let passwords = vec![
755            "pass'word",
756            "pass\"word",
757            "pass$word",
758            "pass`word",
759            "pass\\word",
760        ];
761        for pass in passwords {
762            let result = validate_sudo_password(pass);
763            // Just verify it doesn't panic
764            let _ = result;
765        }
766    }
767
768    #[test]
769    #[ignore = "Uses sudo with wrong password - may lock user out. Run with --ignored"]
770    /// What: Test password validation function signature.
771    ///
772    /// Inputs:
773    /// - Various password strings.
774    ///
775    /// Output:
776    /// - Returns Result<bool, String> as expected.
777    ///
778    /// Details:
779    /// - Verifies the function returns the correct type.
780    /// - Marked as ignored to prevent user lockout from failed sudo attempts.
781    fn test_validate_sudo_password_signature() {
782        let result: Result<bool, String> = validate_sudo_password("test");
783        // Verify it returns the correct type
784        let _ = result;
785    }
786}