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}