1use alloc::vec::Vec;
28
29use azul_core::dom::DomNodeId;
30use azul_core::events::{
31 EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
32};
33use azul_core::task::Instant;
34
35pub use azul_core::biometric::{BiometricKind, BiometricPrompt, BiometricResult};
40
41#[derive(Copy, Debug, Clone, PartialEq, Eq)]
44pub struct BiometricManager {
45 pub last_result: Option<BiometricResult>,
49 pub availability: BiometricKind,
54 pub in_flight: u32,
58 pub pending_event: bool,
64}
65
66impl Default for BiometricManager {
67 fn default() -> Self {
68 Self {
69 last_result: None,
70 availability: BiometricKind::NotAvailable,
71 in_flight: 0,
72 pending_event: false,
73 }
74 }
75}
76
77impl BiometricManager {
78 #[must_use] pub fn new() -> Self {
79 Self::default()
80 }
81
82 #[must_use] pub const fn last_result(&self) -> Option<BiometricResult> {
85 self.last_result
86 }
87
88 #[must_use] pub const fn availability(&self) -> BiometricKind {
91 self.availability
92 }
93
94 #[must_use] pub const fn is_available(&self) -> bool {
96 self.availability.is_available()
97 }
98
99 pub fn set_availability(&mut self, kind: BiometricKind) -> bool {
103 let changed = self.availability != kind;
104 self.availability = kind;
105 changed
106 }
107
108 pub fn set_last_result(&mut self, result: BiometricResult) -> bool {
113 let changed = self.last_result != Some(result);
114 self.last_result = Some(result);
115 self.pending_event = true;
119 self.in_flight = self.in_flight.saturating_sub(1);
120 changed
121 }
122
123 pub const fn mark_requests_dispatched(&mut self, n: u32) {
126 self.in_flight = self.in_flight.saturating_add(n);
127 }
128
129 pub const fn clear_pending_event(&mut self) {
132 self.pending_event = false;
133 }
134
135 #[must_use] pub const fn has_pending_async(&self) -> bool {
138 self.in_flight > 0
139 }
140
141 #[must_use] pub const fn last_was_success(&self) -> bool {
144 matches!(self.last_result, Some(r) if r.is_success())
145 }
146}
147
148impl EventProvider for BiometricManager {
149 fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
153 if self.pending_event {
154 alloc::vec![SyntheticEvent::new(
155 EventType::BiometricResult,
156 CoreEventSource::User,
157 DomNodeId::ROOT,
158 timestamp,
159 EventData::None,
160 )]
161 } else {
162 Vec::new()
163 }
164 }
165}
166
167static PENDING_RESULTS: std::sync::Mutex<Vec<BiometricResult>> =
179 std::sync::Mutex::new(Vec::new());
180
181pub fn push_biometric_result(result: BiometricResult) {
185 let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
186 q.push(result);
187}
188
189pub fn drain_biometric_results() -> Vec<BiometricResult> {
193 let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
194 core::mem::take(&mut *q)
195}
196
197static PENDING_REQUESTS: std::sync::Mutex<Vec<BiometricPrompt>> =
210 std::sync::Mutex::new(Vec::new());
211
212pub fn push_biometric_request(prompt: BiometricPrompt) {
216 let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
217 q.push(prompt);
218}
219
220pub fn drain_biometric_requests() -> Vec<BiometricPrompt> {
224 let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
225 core::mem::take(&mut *q)
226}
227
228pub fn has_queued_requests() -> bool {
236 PENDING_REQUESTS
237 .lock()
238 .map_or_else(|e| !e.into_inner().is_empty(), |q| !q.is_empty())
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn manager_defaults_to_unavailable_and_no_result() {
247 let mgr = BiometricManager::new();
248 assert_eq!(mgr.availability(), BiometricKind::NotAvailable);
249 assert!(!mgr.is_available());
250 assert_eq!(mgr.last_result(), None);
251 assert!(!mgr.last_was_success());
252 }
253
254 #[test]
255 fn set_availability_returns_change_flag() {
256 let mut mgr = BiometricManager::new();
257 assert!(mgr.set_availability(BiometricKind::Face));
258 assert!(mgr.is_available());
259 assert_eq!(mgr.availability(), BiometricKind::Face);
260 assert!(!mgr.set_availability(BiometricKind::Face));
262 assert!(mgr.set_availability(BiometricKind::Fingerprint));
264 }
265
266 #[test]
267 fn set_last_result_returns_change_flag() {
268 let mut mgr = BiometricManager::new();
269 assert!(mgr.set_last_result(BiometricResult::Failed));
270 assert_eq!(mgr.last_result(), Some(BiometricResult::Failed));
271 assert!(!mgr.last_was_success());
272 assert!(!mgr.set_last_result(BiometricResult::Failed));
274 assert!(mgr.set_last_result(BiometricResult::Authenticated));
276 assert!(mgr.last_was_success());
277 }
278
279 #[test]
280 fn passcode_fallback_counts_as_success() {
281 let mut mgr = BiometricManager::new();
282 mgr.set_last_result(BiometricResult::FellBackToPasscode);
283 assert!(mgr.last_was_success());
284 assert!(BiometricResult::FellBackToPasscode.is_success());
285 for r in [
287 BiometricResult::Cancelled,
288 BiometricResult::Failed,
289 BiometricResult::Unavailable,
290 BiometricResult::Error,
291 ] {
292 assert!(!r.is_success(), "{r:?} must not be a success");
293 }
294 }
295
296 #[test]
297 fn async_results_round_trip_through_manager() {
298 let _guard = super::autotest_generated::lock_channels();
301 drop(drain_biometric_results());
302
303 push_biometric_result(BiometricResult::Failed);
304 push_biometric_result(BiometricResult::Authenticated); let drained = drain_biometric_results();
306 assert_eq!(drained.len(), 2, "both parked results drain in order");
307 assert_eq!(drained[0], BiometricResult::Failed);
308 assert_eq!(drained[1], BiometricResult::Authenticated);
309
310 let mut mgr = BiometricManager::new();
312 for r in &drained {
313 mgr.set_last_result(*r);
314 }
315 assert_eq!(
316 mgr.last_result(),
317 Some(BiometricResult::Authenticated),
318 "the last applied result wins"
319 );
320 assert!(mgr.last_was_success());
321
322 assert!(drain_biometric_results().is_empty());
324 }
325
326 #[test]
327 fn requests_round_trip_through_channel() {
328 let _guard = super::autotest_generated::lock_channels();
331 drop(drain_biometric_requests());
332
333 push_biometric_request(BiometricPrompt::new("Unlock A".into()));
334 push_biometric_request(BiometricPrompt::new("Unlock B".into()));
335 let drained = drain_biometric_requests();
336 assert_eq!(drained.len(), 2, "both queued requests drain in order");
337 assert_eq!(drained[0].reason.as_str(), "Unlock A");
338 assert_eq!(drained[1].reason.as_str(), "Unlock B");
339
340 assert!(drain_biometric_requests().is_empty());
342 }
343
344 #[test]
345 fn biometric_prompt_defaults_and_constructor() {
346 let d = BiometricPrompt::default();
347 assert!(!d.allow_device_credential);
348 assert_eq!(d.reason.as_str(), "");
349
350 let p = BiometricPrompt::new("Unlock your vault".into());
351 assert_eq!(p.reason.as_str(), "Unlock your vault");
352 assert_eq!(p.cancel_label.as_str(), "");
353 assert!(!p.allow_device_credential);
354 }
355}
356
357#[cfg(test)]
358mod pump_provider_tests {
359 use super::*;
360 use azul_core::task::{Instant, SystemTick};
361
362 fn ts() -> Instant {
363 Instant::Tick(SystemTick::new(0))
364 }
365
366 #[test]
367 fn in_flight_tracks_dispatch_and_completion() {
368 let mut mgr = BiometricManager::new();
369 assert!(!mgr.has_pending_async());
370 mgr.mark_requests_dispatched(2);
371 assert!(mgr.has_pending_async());
372 mgr.set_last_result(BiometricResult::Cancelled);
373 assert!(mgr.has_pending_async(), "one of two still outstanding");
374 mgr.set_last_result(BiometricResult::Authenticated);
375 assert!(!mgr.has_pending_async());
376 mgr.set_last_result(BiometricResult::Failed);
378 assert!(!mgr.has_pending_async());
379 }
380
381 #[test]
382 fn every_completion_fires_an_event_even_identical_repeats() {
383 let mut mgr = BiometricManager::new();
384 assert!(mgr.get_pending_events(ts()).is_empty());
385 mgr.set_last_result(BiometricResult::Cancelled);
386 assert_eq!(mgr.get_pending_events(ts()).len(), 1);
387 mgr.clear_pending_event();
388 assert!(mgr.get_pending_events(ts()).is_empty());
389 mgr.set_last_result(BiometricResult::Cancelled);
391 assert_eq!(mgr.get_pending_events(ts()).len(), 1);
392 assert_eq!(
393 mgr.get_pending_events(ts())[0].event_type,
394 EventType::BiometricResult
395 );
396 }
397}
398
399#[cfg(test)]
400mod autotest_generated {
401 use alloc::string::{String, ToString};
402 use std::sync::{Mutex, PoisonError};
403
404 use azul_core::task::SystemTick;
405
406 use super::*;
407
408 static CHANNEL_LOCK: Mutex<()> = Mutex::new(());
414
415 pub(super) fn lock_channels() -> std::sync::MutexGuard<'static, ()> {
416 CHANNEL_LOCK.lock().unwrap_or_else(PoisonError::into_inner)
417 }
418
419 fn ts() -> Instant {
420 Instant::Tick(SystemTick::new(0))
421 }
422
423 const ALL_KINDS: [BiometricKind; 4] = [
424 BiometricKind::NotAvailable,
425 BiometricKind::Fingerprint,
426 BiometricKind::Face,
427 BiometricKind::Iris,
428 ];
429
430 const ALL_RESULTS: [BiometricResult; 6] = [
431 BiometricResult::Authenticated,
432 BiometricResult::Failed,
433 BiometricResult::Cancelled,
434 BiometricResult::FellBackToPasscode,
435 BiometricResult::Unavailable,
436 BiometricResult::Error,
437 ];
438
439 #[test]
442 fn new_equals_default_and_holds_the_zero_state_invariants() {
443 let a = BiometricManager::new();
444 let b = BiometricManager::default();
445 assert_eq!(a, b, "new() must be exactly default()");
446 assert_eq!(a.last_result, None);
448 assert_eq!(a.availability, BiometricKind::NotAvailable);
449 assert_eq!(a.in_flight, 0);
450 assert!(!a.pending_event);
451 assert_eq!(a.last_result(), None);
453 assert_eq!(a.availability(), BiometricKind::NotAvailable);
454 assert!(!a.is_available());
455 assert!(!a.has_pending_async());
456 assert!(!a.last_was_success());
457 assert!(
458 a.get_pending_events(ts()).is_empty(),
459 "a fresh manager owes the event pass nothing"
460 );
461 assert_eq!(BiometricManager::new(), BiometricManager::new());
463 }
464
465 #[test]
468 fn getters_mirror_the_public_fields_for_every_kind_result_pair() {
469 for kind in ALL_KINDS {
470 for result in ALL_RESULTS {
471 let mut mgr = BiometricManager::new();
472 mgr.set_availability(kind);
473 mgr.set_last_result(result);
474
475 assert_eq!(mgr.availability(), kind, "{kind:?}");
476 assert_eq!(mgr.availability(), mgr.availability, "{kind:?}");
477 assert_eq!(
478 mgr.is_available(),
479 kind.is_available(),
480 "is_available must delegate to the kind ({kind:?})"
481 );
482 assert_eq!(mgr.last_result(), Some(result), "{result:?}");
483 assert_eq!(mgr.last_result(), mgr.last_result, "{result:?}");
484 assert_eq!(
485 mgr.last_was_success(),
486 result.is_success(),
487 "last_was_success must delegate to the result ({result:?})"
488 );
489 assert_eq!(mgr.availability(), kind);
492 }
493 }
494 }
495
496 #[test]
497 fn is_available_is_true_for_every_real_sensor_and_false_only_for_none() {
498 let mut mgr = BiometricManager::new();
499 assert!(!mgr.is_available(), "known-false: the default");
500 mgr.set_availability(BiometricKind::Fingerprint);
501 assert!(mgr.is_available(), "known-true: a real sensor");
502
503 for kind in ALL_KINDS {
504 let mut mgr = BiometricManager::new();
505 mgr.set_availability(kind);
506 assert_eq!(
507 mgr.is_available(),
508 kind != BiometricKind::NotAvailable,
509 "only NotAvailable is unavailable ({kind:?})"
510 );
511 }
512 }
513
514 #[test]
515 fn last_was_success_is_false_while_no_request_has_resolved() {
516 let mgr = BiometricManager::new();
518 assert_eq!(mgr.last_result(), None);
519 assert!(
520 !mgr.last_was_success(),
521 "an unauthenticated vault must stay locked before the first attempt"
522 );
523 let mut mgr = BiometricManager::new();
525 mgr.set_availability(BiometricKind::Face);
526 assert!(mgr.is_available());
527 assert!(!mgr.last_was_success());
528 }
529
530 #[test]
531 fn only_authenticated_and_passcode_fallback_unlock_the_gate() {
532 for result in ALL_RESULTS {
533 let mut mgr = BiometricManager::new();
534 mgr.set_last_result(result);
535 let expected = matches!(
536 result,
537 BiometricResult::Authenticated | BiometricResult::FellBackToPasscode
538 );
539 assert_eq!(
540 mgr.last_was_success(),
541 expected,
542 "{result:?} must {} unlock the gate",
543 if expected { "" } else { "not" }
544 );
545 }
546 }
547
548 #[test]
551 fn set_availability_change_flag_is_exactly_inequality_for_all_pairs() {
552 for from in ALL_KINDS {
553 for to in ALL_KINDS {
554 let mut mgr = BiometricManager::new();
555 mgr.availability = from;
556 let changed = mgr.set_availability(to);
557 assert_eq!(
558 changed,
559 from != to,
560 "set_availability({to:?}) over {from:?} reported the wrong change flag"
561 );
562 assert_eq!(mgr.availability(), to, "the write must land regardless");
563 assert!(!mgr.set_availability(to), "idempotent re-apply of {to:?}");
565 assert_eq!(mgr.availability(), to);
566 }
567 }
568 }
569
570 #[test]
571 fn set_availability_does_not_disturb_the_async_or_result_state() {
572 let mut mgr = BiometricManager::new();
573 mgr.mark_requests_dispatched(3);
574 mgr.set_last_result(BiometricResult::Authenticated);
575 mgr.clear_pending_event();
576 let before = mgr;
577
578 mgr.set_availability(BiometricKind::Iris);
579
580 assert_eq!(mgr.last_result, before.last_result, "result untouched");
581 assert_eq!(mgr.in_flight, before.in_flight, "in_flight untouched");
582 assert_eq!(
583 mgr.pending_event, before.pending_event,
584 "an availability probe is not a prompt outcome — it must not fire an event"
585 );
586 }
587
588 #[test]
589 fn set_last_result_change_flag_is_exactly_inequality_for_all_pairs() {
590 for first in ALL_RESULTS {
591 for second in ALL_RESULTS {
592 let mut mgr = BiometricManager::new();
593 assert!(
594 mgr.set_last_result(first),
595 "None -> Some({first:?}) is always a change"
596 );
597 let changed = mgr.set_last_result(second);
598 assert_eq!(
599 changed,
600 first != second,
601 "set_last_result({second:?}) over {first:?} reported the wrong change flag"
602 );
603 assert_eq!(mgr.last_result(), Some(second), "the write must land");
604 }
605 }
606 }
607
608 #[test]
609 fn every_completion_arms_a_pending_event_even_when_unchanged() {
610 for result in ALL_RESULTS {
614 let mut mgr = BiometricManager::new();
615 mgr.set_last_result(result);
616 mgr.clear_pending_event();
617
618 let changed = mgr.set_last_result(result);
619 assert!(!changed, "identical outcome is not a state change ({result:?})");
620 assert!(
621 mgr.pending_event,
622 "a repeated {result:?} must still fire an event"
623 );
624 assert_eq!(mgr.get_pending_events(ts()).len(), 1);
625 }
626 }
627
628 #[test]
631 fn mark_requests_dispatched_zero_is_a_no_op() {
632 let mut mgr = BiometricManager::new();
633 mgr.mark_requests_dispatched(0);
634 assert_eq!(mgr.in_flight, 0, "0 prompts dispatched adds nothing");
635 assert!(
636 !mgr.has_pending_async(),
637 "dispatching nothing must not arm the pump's timer"
638 );
639
640 mgr.mark_requests_dispatched(1);
641 mgr.mark_requests_dispatched(0);
642 assert_eq!(mgr.in_flight, 1, "0 must not clear an outstanding prompt");
643 assert!(mgr.has_pending_async());
644 }
645
646 #[test]
647 fn mark_requests_dispatched_saturates_at_u32_max_instead_of_overflowing() {
648 let mut mgr = BiometricManager::new();
649 mgr.mark_requests_dispatched(u32::MAX);
650 assert_eq!(mgr.in_flight, u32::MAX);
651 assert!(mgr.has_pending_async());
652
653 mgr.mark_requests_dispatched(1);
655 assert_eq!(mgr.in_flight, u32::MAX, "saturates, does not wrap to 0");
656 mgr.mark_requests_dispatched(u32::MAX);
657 assert_eq!(mgr.in_flight, u32::MAX, "still clamped");
658 assert!(
659 mgr.has_pending_async(),
660 "wrapping to 0 here would disarm the pump and strand the reply"
661 );
662 }
663
664 #[test]
665 fn mark_requests_dispatched_accumulates_across_calls() {
666 let mut mgr = BiometricManager::new();
667 for _ in 0..10u32 {
668 mgr.mark_requests_dispatched(7);
669 }
670 assert_eq!(mgr.in_flight, 70);
671 let mut mgr = BiometricManager::new();
673 mgr.mark_requests_dispatched(u32::MAX - 1);
674 mgr.mark_requests_dispatched(1);
675 assert_eq!(mgr.in_flight, u32::MAX);
676 }
677
678 #[test]
679 fn unsolicited_results_saturate_in_flight_at_zero_instead_of_underflowing() {
680 let mut mgr = BiometricManager::new();
684 assert_eq!(mgr.in_flight, 0);
685 for result in ALL_RESULTS {
686 mgr.set_last_result(result);
687 assert_eq!(mgr.in_flight, 0, "underflow on unsolicited {result:?}");
688 assert!(!mgr.has_pending_async());
689 }
690 }
691
692 #[test]
693 fn each_completion_retires_exactly_one_in_flight_prompt() {
694 let mut mgr = BiometricManager::new();
695 mgr.mark_requests_dispatched(3);
696 assert_eq!(mgr.in_flight, 3);
697
698 mgr.set_last_result(BiometricResult::Cancelled);
699 assert_eq!(mgr.in_flight, 2);
700 assert!(mgr.has_pending_async());
701 mgr.set_last_result(BiometricResult::Failed);
702 assert_eq!(mgr.in_flight, 1);
703 assert!(mgr.has_pending_async());
704 mgr.set_last_result(BiometricResult::Authenticated);
705 assert_eq!(mgr.in_flight, 0);
706 assert!(!mgr.has_pending_async(), "all three outcomes folded back");
707
708 let mut mgr = BiometricManager::new();
710 mgr.mark_requests_dispatched(u32::MAX);
711 mgr.set_last_result(BiometricResult::Error);
712 assert_eq!(mgr.in_flight, u32::MAX - 1);
713 }
714
715 #[test]
716 fn has_pending_async_is_true_exactly_while_in_flight_is_non_zero() {
717 for n in [0u32, 1, 2, u32::MAX - 1, u32::MAX] {
718 let mut mgr = BiometricManager::new();
719 mgr.mark_requests_dispatched(n);
720 assert_eq!(
721 mgr.has_pending_async(),
722 n > 0,
723 "has_pending_async disagreed with in_flight = {n}"
724 );
725 }
726 }
727
728 #[test]
731 fn clear_pending_event_is_idempotent_and_touches_nothing_else() {
732 let mut mgr = BiometricManager::new();
733 mgr.set_availability(BiometricKind::Face);
734 mgr.mark_requests_dispatched(2);
735 mgr.set_last_result(BiometricResult::Authenticated);
736 assert!(mgr.pending_event);
737
738 mgr.clear_pending_event();
739 assert!(!mgr.pending_event);
740 let after_first = mgr;
741
742 mgr.clear_pending_event();
744 assert_eq!(mgr, after_first, "the second clear is a no-op");
745 assert!(mgr.get_pending_events(ts()).is_empty());
746
747 assert_eq!(mgr.last_result(), Some(BiometricResult::Authenticated));
749 assert!(mgr.last_was_success(), "clearing the event must not re-lock the vault");
750 assert_eq!(mgr.availability(), BiometricKind::Face);
751 assert_eq!(mgr.in_flight, 1);
752 assert!(mgr.has_pending_async(), "the second prompt is still outstanding");
753 }
754
755 #[test]
756 fn clear_pending_event_on_a_fresh_manager_is_a_no_op() {
757 let mut mgr = BiometricManager::new();
758 mgr.clear_pending_event();
759 assert_eq!(mgr, BiometricManager::new());
760 }
761
762 #[test]
765 fn pending_event_yields_exactly_one_root_event_and_is_not_consuming() {
766 let mut mgr = BiometricManager::new();
767 mgr.set_last_result(BiometricResult::Authenticated);
768
769 let events = mgr.get_pending_events(ts());
770 assert_eq!(events.len(), 1);
771 assert_eq!(events[0].event_type, EventType::BiometricResult);
772 assert_eq!(events[0].source, CoreEventSource::User);
773 assert_eq!(
774 events[0].target,
775 DomNodeId::ROOT,
776 "the outcome is window-level, not node-level"
777 );
778 assert_eq!(events[0].data, EventData::None);
779 assert_eq!(events[0].timestamp, ts(), "the caller's timestamp is preserved");
780
781 assert_eq!(mgr.get_pending_events(ts()).len(), 1);
783 mgr.clear_pending_event();
784 assert!(mgr.get_pending_events(ts()).is_empty());
785 }
786
787 #[test]
790 fn manager_is_copy_so_a_snapshot_never_sees_later_mutations() {
791 let mut original = BiometricManager::new();
795 original.mark_requests_dispatched(1);
796
797 let mut snapshot = original;
798 snapshot.set_last_result(BiometricResult::Authenticated);
799 snapshot.set_availability(BiometricKind::Iris);
800
801 assert_eq!(original.last_result(), None, "the original is untouched");
802 assert_eq!(original.availability(), BiometricKind::NotAvailable);
803 assert_eq!(original.in_flight, 1);
804 assert!(!original.pending_event);
805
806 assert_eq!(snapshot.last_result(), Some(BiometricResult::Authenticated));
807 assert_eq!(snapshot.in_flight, 0);
808 }
809
810 #[test]
813 fn results_channel_round_trips_every_variant_in_arrival_order() {
814 let _guard = lock_channels();
815 drop(drain_biometric_results());
816
817 for result in ALL_RESULTS {
818 push_biometric_result(result);
819 }
820 let drained = drain_biometric_results();
821 assert_eq!(drained.len(), ALL_RESULTS.len(), "no result is dropped");
822 assert_eq!(
823 drained.as_slice(),
824 ALL_RESULTS.as_slice(),
825 "the channel is FIFO — the layout pass relies on last-one-wins"
826 );
827 assert!(
828 drain_biometric_results().is_empty(),
829 "draining takes the queue, it does not copy it"
830 );
831 }
832
833 #[test]
834 fn draining_an_empty_results_channel_repeatedly_is_safe() {
835 let _guard = lock_channels();
836 drop(drain_biometric_results());
837 for _ in 0..3 {
838 assert!(drain_biometric_results().is_empty());
839 }
840 }
841
842 #[test]
843 fn duplicate_results_are_all_preserved_in_the_channel() {
844 let _guard = lock_channels();
847 drop(drain_biometric_results());
848
849 for _ in 0..64 {
850 push_biometric_result(BiometricResult::Failed);
851 }
852 let drained = drain_biometric_results();
853 assert_eq!(drained.len(), 64);
854 assert!(drained.iter().all(|r| *r == BiometricResult::Failed));
855
856 let mut mgr = BiometricManager::new();
859 mgr.mark_requests_dispatched(2);
860 for r in &drained {
861 mgr.set_last_result(*r);
862 }
863 assert_eq!(mgr.in_flight, 0, "saturating_sub floors at zero");
864 assert_eq!(mgr.last_result(), Some(BiometricResult::Failed));
865 }
866
867 #[test]
868 fn results_pushed_from_many_threads_are_all_delivered() {
869 let _guard = lock_channels();
870 drop(drain_biometric_results());
871
872 let threads: Vec<_> = (0..4)
873 .map(|_| {
874 std::thread::spawn(|| {
875 for _ in 0..25 {
876 push_biometric_result(BiometricResult::Authenticated);
877 }
878 })
879 })
880 .collect();
881 for t in threads {
882 t.join().expect("a backend thread panicked while parking a result");
883 }
884
885 let drained = drain_biometric_results();
886 assert_eq!(drained.len(), 100, "no result lost across 4 backend threads");
887 assert!(drained.iter().all(|r| r.is_success()));
888 assert!(drain_biometric_results().is_empty());
889 }
890
891 #[test]
894 fn has_queued_requests_tracks_the_queue_exactly() {
895 let _guard = lock_channels();
896 drop(drain_biometric_requests());
897 assert!(
898 !has_queued_requests(),
899 "known-false: nothing parked after a drain"
900 );
901
902 push_biometric_request(BiometricPrompt::new("Unlock".into()));
903 assert!(has_queued_requests(), "known-true: one prompt parked");
904 assert!(has_queued_requests());
906
907 push_biometric_request(BiometricPrompt::default());
908 assert!(has_queued_requests());
909
910 let drained = drain_biometric_requests();
911 assert_eq!(drained.len(), 2);
912 assert!(
913 !has_queued_requests(),
914 "the pump's arming check must go quiet once the prompts are dispatched"
915 );
916 }
917
918 #[test]
919 fn request_channel_round_trips_hostile_prompt_strings_byte_for_byte() {
920 let _guard = lock_channels();
921 drop(drain_biometric_requests());
922
923 let hostile: Vec<String> = alloc::vec![
924 String::new(), "Entsperre den Tresor 🔐".to_string(), "افتح الخزنة".to_string(), "e\u{0301}\u{0301}\u{0301}".to_string(), "line\nbreak\ttab\r\n".to_string(), "nul\0inside".to_string(), "%s %n {} \\0 ${x}".to_string(), "u".repeat(64 * 1024), ];
933
934 for reason in &hostile {
935 push_biometric_request(BiometricPrompt {
936 reason: reason.clone().into(),
937 cancel_label: reason.clone().into(),
938 allow_device_credential: true,
939 });
940 }
941
942 let drained = drain_biometric_requests();
943 assert_eq!(drained.len(), hostile.len(), "no prompt is dropped");
944 for (prompt, expected) in drained.iter().zip(hostile.iter()) {
945 assert_eq!(
946 prompt.reason.as_str(),
947 expected.as_str(),
948 "the reason must survive the channel unmangled"
949 );
950 assert_eq!(prompt.cancel_label.as_str(), expected.as_str());
951 assert!(prompt.allow_device_credential);
952 }
953 assert_eq!(
954 drained.last().unwrap().reason.as_str().len(),
955 64 * 1024,
956 "a 64 KiB reason is not truncated"
957 );
958 assert!(!has_queued_requests());
959 }
960
961 #[test]
962 fn requests_pushed_from_many_threads_are_all_delivered() {
963 let _guard = lock_channels();
964 drop(drain_biometric_requests());
965
966 let threads: Vec<_> = (0..4)
967 .map(|i| {
968 std::thread::spawn(move || {
969 for j in 0..25 {
970 push_biometric_request(BiometricPrompt::new(
971 alloc::format!("t{i}-{j}").into(),
972 ));
973 }
974 })
975 })
976 .collect();
977 for t in threads {
978 t.join().expect("a callback thread panicked while queueing a prompt");
979 }
980
981 let drained = drain_biometric_requests();
982 assert_eq!(drained.len(), 100, "no prompt lost across 4 callback threads");
983 for i in 0..4 {
985 let mine: Vec<_> = drained
986 .iter()
987 .filter(|p| p.reason.as_str().starts_with(&alloc::format!("t{i}-")))
988 .collect();
989 assert_eq!(mine.len(), 25, "thread {i} lost a prompt");
990 for (j, p) in mine.iter().enumerate() {
991 assert_eq!(p.reason.as_str(), alloc::format!("t{i}-{j}"));
992 }
993 }
994 assert!(!has_queued_requests());
995 }
996
997 #[test]
1000 fn full_request_dispatch_result_loop_leaves_the_pump_disarmed() {
1001 let _guard = lock_channels();
1002 drop(drain_biometric_requests());
1003 drop(drain_biometric_results());
1004
1005 let mut mgr = BiometricManager::new();
1006 mgr.set_availability(BiometricKind::Fingerprint);
1007
1008 push_biometric_request(BiometricPrompt::new("Unlock A".into()));
1010 push_biometric_request(BiometricPrompt::new("Unlock B".into()));
1011 assert!(
1012 has_queued_requests(),
1013 "queued-but-undispatched prompts must arm the pump on their own — \
1014 has_pending_async() cannot see them yet"
1015 );
1016 assert!(!mgr.has_pending_async());
1017
1018 let requests = drain_biometric_requests();
1020 assert_eq!(requests.len(), 2);
1021 mgr.mark_requests_dispatched(u32::try_from(requests.len()).unwrap());
1022 assert!(!has_queued_requests());
1023 assert!(mgr.has_pending_async(), "now the in-flight count arms the pump");
1024
1025 push_biometric_result(BiometricResult::Failed);
1027 push_biometric_result(BiometricResult::FellBackToPasscode);
1028 for r in drain_biometric_results() {
1029 mgr.set_last_result(r);
1030 }
1031
1032 assert_eq!(mgr.in_flight, 0);
1033 assert!(!mgr.has_pending_async(), "pump disarms once every outcome folded");
1034 assert!(mgr.pending_event);
1035 assert_eq!(mgr.get_pending_events(ts()).len(), 1);
1036 assert_eq!(mgr.last_result(), Some(BiometricResult::FellBackToPasscode));
1037 assert!(mgr.last_was_success(), "the passcode fallback unlocks the vault");
1038
1039 mgr.clear_pending_event();
1040 assert!(mgr.get_pending_events(ts()).is_empty());
1041 }
1042}