1use std::{
43 pin::pin,
44 sync::{
45 Arc, Mutex,
46 atomic::{AtomicU8, Ordering},
47 },
48 time::Duration,
49};
50
51pub type AuthResultSender = tokio::sync::oneshot::Sender<Result<(), String>>;
52pub type AuthResultReceiver = tokio::sync::oneshot::Receiver<Result<(), String>>;
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56#[repr(u8)]
57pub enum AuthState {
58 #[default]
60 Unauthenticated = 0,
61 Authenticated = 1,
63 Failed = 2,
65}
66
67impl AuthState {
68 #[inline]
69 #[must_use]
70 #[expect(
71 clippy::match_same_arms,
72 reason = "explicit variant listing is clearer than collapsing 0 with wildcard"
73 )]
74 fn from_u8(value: u8) -> Self {
75 match value {
76 0 => Self::Unauthenticated,
77 1 => Self::Authenticated,
78 2 => Self::Failed,
79 _ => Self::Unauthenticated,
80 }
81 }
82
83 #[inline]
84 #[must_use]
85 const fn as_u8(self) -> u8 {
86 self as u8
87 }
88}
89
90#[derive(Clone, Debug)]
113pub struct AuthTracker {
114 tx: Arc<Mutex<Option<AuthResultSender>>>,
115 state: Arc<AtomicU8>,
116 state_notify: Arc<tokio::sync::Notify>,
117}
118
119impl AuthTracker {
120 #[must_use]
122 pub fn new() -> Self {
123 Self {
124 tx: Arc::new(Mutex::new(None)),
125 state: Arc::new(AtomicU8::new(AuthState::Unauthenticated.as_u8())),
126 state_notify: Arc::new(tokio::sync::Notify::new()),
127 }
128 }
129
130 #[must_use]
132 pub fn auth_state(&self) -> AuthState {
133 AuthState::from_u8(self.state.load(Ordering::Acquire))
134 }
135
136 #[must_use]
138 pub fn is_authenticated(&self) -> bool {
139 self.auth_state() == AuthState::Authenticated
140 }
141
142 pub fn invalidate(&self) {
147 self.state
148 .store(AuthState::Unauthenticated.as_u8(), Ordering::Release);
149 self.state_notify.notify_waiters();
150 }
151
152 #[allow(
161 clippy::must_use_candidate,
162 reason = "callers use this for side effects"
163 )]
164 pub fn begin(&self) -> AuthResultReceiver {
165 let (sender, receiver) = tokio::sync::oneshot::channel();
166 self.state
167 .store(AuthState::Unauthenticated.as_u8(), Ordering::Release);
168
169 if let Ok(mut guard) = self.tx.lock() {
170 if let Some(old) = guard.take() {
171 log::warn!("New authentication request superseding previous pending request");
172 let _ = old.send(Err("Authentication attempt superseded".to_string()));
173 } else {
174 log::debug!("Starting new authentication request");
175 }
176 *guard = Some(sender);
177 }
178
179 receiver
180 }
181
182 pub fn succeed(&self) {
191 self.state
192 .store(AuthState::Authenticated.as_u8(), Ordering::Release);
193 self.state_notify.notify_waiters();
194
195 if let Ok(mut guard) = self.tx.lock()
196 && let Some(sender) = guard.take()
197 {
198 let _ = sender.send(Ok(()));
199 }
200 }
201
202 pub fn fail(&self, error: impl Into<String>) {
211 self.state
212 .store(AuthState::Failed.as_u8(), Ordering::Release);
213 self.state_notify.notify_waiters();
214 let message = error.into();
215
216 if let Ok(mut guard) = self.tx.lock()
217 && let Some(sender) = guard.take()
218 {
219 let _ = sender.send(Err(message));
220 }
221 }
222
223 pub async fn wait_for_result<E>(
240 &self,
241 timeout: Duration,
242 receiver: AuthResultReceiver,
243 ) -> Result<(), E>
244 where
245 E: From<String>,
246 {
247 match tokio::time::timeout(timeout, receiver).await {
248 Ok(Ok(Ok(()))) => Ok(()),
249 Ok(Ok(Err(msg))) => Err(E::from(msg)),
250 Ok(Err(_)) => Err(E::from("Authentication channel closed".to_string())),
251 Err(_) => {
252 Err(E::from("Authentication timed out".to_string()))
256 }
257 }
258 }
259
260 pub async fn wait_for_authenticated(&self, timeout: Duration) -> bool {
274 if self.is_authenticated() {
275 return true;
276 }
277
278 tokio::time::timeout(timeout, async {
279 loop {
280 let mut notified = pin!(self.state_notify.notified());
282 notified.as_mut().enable();
283
284 match self.auth_state() {
285 AuthState::Authenticated => return true,
286 AuthState::Failed => return false,
287 AuthState::Unauthenticated => notified.await,
288 }
289 }
290 })
291 .await
292 .unwrap_or(false)
293 }
294}
295
296impl Default for AuthTracker {
297 fn default() -> Self {
298 Self::new()
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use std::{
305 sync::atomic::{AtomicBool, Ordering},
306 time::Duration,
307 };
308
309 use rstest::rstest;
310
311 use super::*;
312
313 #[derive(Debug, PartialEq)]
314 struct TestError(String);
315
316 impl From<String> for TestError {
317 fn from(msg: String) -> Self {
318 Self(msg)
319 }
320 }
321
322 #[rstest]
323 #[tokio::test]
324 async fn test_successful_authentication() {
325 let tracker = AuthTracker::new();
326 let rx = tracker.begin();
327
328 tracker.succeed();
329
330 let result: Result<(), TestError> =
331 tracker.wait_for_result(Duration::from_secs(1), rx).await;
332
333 assert!(result.is_ok());
334 }
335
336 #[rstest]
337 #[tokio::test]
338 async fn test_failed_authentication() {
339 let tracker = AuthTracker::new();
340 let rx = tracker.begin();
341
342 tracker.fail("Invalid credentials");
343
344 let result: Result<(), TestError> =
345 tracker.wait_for_result(Duration::from_secs(1), rx).await;
346
347 assert_eq!(
348 result.unwrap_err(),
349 TestError("Invalid credentials".to_string())
350 );
351 }
352
353 #[rstest]
354 #[tokio::test]
355 async fn test_authentication_timeout() {
356 let tracker = AuthTracker::new();
357 let rx = tracker.begin();
358
359 let result: Result<(), TestError> =
362 tracker.wait_for_result(Duration::from_millis(50), rx).await;
363
364 assert_eq!(
365 result.unwrap_err(),
366 TestError("Authentication timed out".to_string())
367 );
368 }
369
370 #[rstest]
371 #[tokio::test]
372 async fn test_begin_supersedes_previous_sender() {
373 let tracker = AuthTracker::new();
374
375 let first = tracker.begin();
376 let second = tracker.begin();
377
378 let result = first.await.expect("oneshot closed unexpectedly");
380 assert_eq!(result, Err("Authentication attempt superseded".to_string()));
381
382 tracker.succeed();
384 let result: Result<(), TestError> = tracker
385 .wait_for_result(Duration::from_secs(1), second)
386 .await;
387
388 assert!(result.is_ok());
389 }
390
391 #[rstest]
392 #[tokio::test]
393 async fn test_succeed_without_pending_auth() {
394 let tracker = AuthTracker::new();
395
396 tracker.succeed();
398 }
399
400 #[rstest]
401 #[tokio::test]
402 async fn test_fail_without_pending_auth() {
403 let tracker = AuthTracker::new();
404
405 tracker.fail("Some error");
407 }
408
409 #[rstest]
410 #[tokio::test]
411 async fn test_multiple_sequential_authentications() {
412 let tracker = AuthTracker::new();
413
414 let rx1 = tracker.begin();
416 tracker.succeed();
417 let result1: Result<(), TestError> =
418 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
419 assert!(result1.is_ok());
420
421 let rx2 = tracker.begin();
423 tracker.fail("Credentials expired");
424 let result2: Result<(), TestError> =
425 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
426 assert_eq!(
427 result2.unwrap_err(),
428 TestError("Credentials expired".to_string())
429 );
430
431 let rx3 = tracker.begin();
433 tracker.succeed();
434 let result3: Result<(), TestError> =
435 tracker.wait_for_result(Duration::from_secs(1), rx3).await;
436 assert!(result3.is_ok());
437 }
438
439 #[rstest]
440 #[tokio::test]
441 async fn test_channel_closed_before_result() {
442 let tracker = AuthTracker::new();
443 let rx = tracker.begin();
444
445 tracker.begin();
447
448 let result: Result<(), TestError> =
450 tracker.wait_for_result(Duration::from_secs(1), rx).await;
451
452 assert_eq!(
453 result.unwrap_err(),
454 TestError("Authentication attempt superseded".to_string())
455 );
456 }
457
458 #[rstest]
459 #[tokio::test]
460 async fn test_concurrent_auth_attempts() {
461 let tracker = Arc::new(AuthTracker::new());
462 let mut handles = vec![];
463
464 for i in 0..10 {
466 let tracker_clone = Arc::clone(&tracker);
467 let handle = tokio::spawn(async move {
468 let rx = tracker_clone.begin();
469
470 if i == 9 {
472 tokio::time::sleep(Duration::from_millis(10)).await;
473 tracker_clone.succeed();
474 }
475
476 let result: Result<(), TestError> = tracker_clone
477 .wait_for_result(Duration::from_secs(1), rx)
478 .await;
479
480 (i, result)
481 });
482 handles.push(handle);
483 }
484
485 let mut successes = 0;
486 let mut superseded = 0;
487
488 for handle in handles {
489 let (i, result) = handle.await.unwrap();
490 match result {
491 Ok(()) => {
492 assert_eq!(i, 9);
494 successes += 1;
495 }
496 Err(TestError(msg)) if msg.contains("superseded") => {
497 superseded += 1;
498 }
499 Err(e) => panic!("Unexpected error: {e:?}"),
500 }
501 }
502
503 assert_eq!(successes, 1);
504 assert_eq!(superseded, 9);
505 }
506
507 #[rstest]
508 fn test_default_trait() {
509 let _tracker = AuthTracker::default();
510 }
511
512 #[rstest]
513 #[tokio::test]
514 async fn test_clone_trait() {
515 let tracker = AuthTracker::new();
516 let cloned = tracker.clone();
517
518 let rx = tracker.begin();
520 cloned.succeed(); let result: Result<(), TestError> =
522 tracker.wait_for_result(Duration::from_secs(1), rx).await;
523 assert!(result.is_ok());
524 }
525
526 #[rstest]
527 fn test_debug_trait() {
528 let tracker = AuthTracker::new();
529 let debug_str = format!("{tracker:?}");
530 assert!(debug_str.contains("AuthTracker"));
531 }
532
533 #[rstest]
534 #[tokio::test]
535 async fn test_timeout_clears_sender() {
536 let tracker = AuthTracker::new();
537
538 let rx1 = tracker.begin();
540 let result1: Result<(), TestError> = tracker
541 .wait_for_result(Duration::from_millis(50), rx1)
542 .await;
543 assert_eq!(
544 result1.unwrap_err(),
545 TestError("Authentication timed out".to_string())
546 );
547
548 let rx2 = tracker.begin();
550 tracker.succeed();
551 let result2: Result<(), TestError> =
552 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
553 assert!(result2.is_ok());
554 }
555
556 #[rstest]
557 #[tokio::test]
558 async fn test_fail_clears_sender() {
559 let tracker = AuthTracker::new();
560
561 let rx1 = tracker.begin();
563 tracker.fail("Bad credentials");
564 let result1: Result<(), TestError> =
565 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
566 assert!(result1.is_err());
567
568 let rx2 = tracker.begin();
570 tracker.succeed();
571 let result2: Result<(), TestError> =
572 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
573 assert!(result2.is_ok());
574 }
575
576 #[rstest]
577 #[tokio::test]
578 async fn test_succeed_clears_sender() {
579 let tracker = AuthTracker::new();
580
581 let rx1 = tracker.begin();
583 tracker.succeed();
584 let result1: Result<(), TestError> =
585 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
586 assert!(result1.is_ok());
587
588 let rx2 = tracker.begin();
590 tracker.succeed();
591 let result2: Result<(), TestError> =
592 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
593 assert!(result2.is_ok());
594 }
595
596 #[rstest]
597 #[tokio::test]
598 async fn test_rapid_begin_succeed_cycles() {
599 let tracker = AuthTracker::new();
600
601 for _ in 0..100 {
603 let rx = tracker.begin();
604 tracker.succeed();
605 let result: Result<(), TestError> =
606 tracker.wait_for_result(Duration::from_secs(1), rx).await;
607 assert!(result.is_ok());
608 }
609 }
610
611 #[rstest]
612 #[tokio::test]
613 async fn test_double_succeed_is_safe() {
614 let tracker = AuthTracker::new();
615 let rx = tracker.begin();
616
617 tracker.succeed();
619 tracker.succeed(); let result: Result<(), TestError> =
622 tracker.wait_for_result(Duration::from_secs(1), rx).await;
623 assert!(result.is_ok());
624 }
625
626 #[rstest]
627 #[tokio::test]
628 async fn test_double_fail_is_safe() {
629 let tracker = AuthTracker::new();
630 let rx = tracker.begin();
631
632 tracker.fail("Error 1");
634 tracker.fail("Error 2"); let result: Result<(), TestError> =
637 tracker.wait_for_result(Duration::from_secs(1), rx).await;
638 assert_eq!(
639 result.unwrap_err(),
640 TestError("Error 1".to_string()) );
642 }
643
644 #[rstest]
645 #[tokio::test]
646 async fn test_succeed_after_fail_is_ignored() {
647 let tracker = AuthTracker::new();
648 let rx = tracker.begin();
649
650 tracker.fail("Auth failed");
651 tracker.succeed(); let result: Result<(), TestError> =
654 tracker.wait_for_result(Duration::from_secs(1), rx).await;
655 assert!(result.is_err()); }
657
658 #[rstest]
659 #[tokio::test]
660 async fn test_fail_after_succeed_is_ignored() {
661 let tracker = AuthTracker::new();
662 let rx = tracker.begin();
663
664 tracker.succeed();
665 tracker.fail("Auth failed"); let result: Result<(), TestError> =
668 tracker.wait_for_result(Duration::from_secs(1), rx).await;
669 assert!(result.is_ok()); }
671
672 #[rstest]
679 #[tokio::test]
680 async fn test_reconnect_flow_waits_for_auth() {
681 let tracker = Arc::new(AuthTracker::new());
682 let subscribed = Arc::new(tokio::sync::Notify::new());
683 let auth_completed = Arc::new(tokio::sync::Notify::new());
684
685 let tracker_reconnect = Arc::clone(&tracker);
687 let subscribed_reconnect = Arc::clone(&subscribed);
688 let auth_completed_reconnect = Arc::clone(&auth_completed);
689
690 let reconnect_task = tokio::spawn(async move {
691 let rx = tracker_reconnect.begin();
693
694 let tracker_resub = Arc::clone(&tracker_reconnect);
696 let subscribed_resub = Arc::clone(&subscribed_reconnect);
697 let auth_completed_resub = Arc::clone(&auth_completed_reconnect);
698
699 let resub_task = tokio::spawn(async move {
700 let result: Result<(), TestError> = tracker_resub
702 .wait_for_result(Duration::from_secs(5), rx)
703 .await;
704
705 if result.is_ok() {
706 auth_completed_resub.notify_one();
707 tokio::time::sleep(Duration::from_millis(10)).await;
709 subscribed_resub.notify_one();
710 }
711 });
712
713 resub_task.await.unwrap();
714 });
715
716 tokio::time::sleep(Duration::from_millis(100)).await;
718 tracker.succeed();
719
720 reconnect_task.await.unwrap();
722
723 tokio::select! {
725 () = auth_completed.notified() => {
726 }
728 () = tokio::time::sleep(Duration::from_secs(1)) => {
729 panic!("Auth never completed");
730 }
731 }
732
733 tokio::select! {
735 () = subscribed.notified() => {
736 }
738 () = tokio::time::sleep(Duration::from_secs(1)) => {
739 panic!("Subscription never completed");
740 }
741 }
742 }
743
744 #[rstest]
746 #[tokio::test]
747 async fn test_reconnect_flow_blocks_on_auth_failure() {
748 let tracker = Arc::new(AuthTracker::new());
749 let subscribed = Arc::new(AtomicBool::new(false));
750
751 let tracker_reconnect = Arc::clone(&tracker);
752 let subscribed_reconnect = Arc::clone(&subscribed);
753
754 let reconnect_task = tokio::spawn(async move {
755 let rx = tracker_reconnect.begin();
756
757 let tracker_resub = Arc::clone(&tracker_reconnect);
759 let subscribed_resub = Arc::clone(&subscribed_reconnect);
760
761 let resub_task = tokio::spawn(async move {
762 let result: Result<(), TestError> = tracker_resub
763 .wait_for_result(Duration::from_secs(5), rx)
764 .await;
765
766 if result.is_ok() {
768 subscribed_resub.store(true, Ordering::Relaxed);
769 }
770 });
771
772 resub_task.await.unwrap();
773 });
774
775 tokio::time::sleep(Duration::from_millis(50)).await;
777 tracker.fail("Invalid credentials");
778
779 reconnect_task.await.unwrap();
781
782 tokio::time::sleep(Duration::from_millis(100)).await;
784 assert!(!subscribed.load(Ordering::Relaxed));
785 }
786
787 #[rstest]
789 #[tokio::test]
790 async fn test_state_machine_transitions() {
791 let tracker = AuthTracker::new();
792
793 let rx1 = tracker.begin();
795
796 tracker.succeed();
798 let result1: Result<(), TestError> =
799 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
800 assert!(result1.is_ok());
801
802 let rx2 = tracker.begin();
804
805 tracker.fail("Error");
807 let result2: Result<(), TestError> =
808 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
809 assert!(result2.is_err());
810
811 let rx3 = tracker.begin();
813
814 let result3: Result<(), TestError> = tracker
816 .wait_for_result(Duration::from_millis(50), rx3)
817 .await;
818 assert_eq!(
819 result3.unwrap_err(),
820 TestError("Authentication timed out".to_string())
821 );
822
823 let rx4 = tracker.begin();
825
826 let rx5 = tracker.begin();
828 let result4: Result<(), TestError> =
829 tracker.wait_for_result(Duration::from_secs(1), rx4).await;
830 assert_eq!(
831 result4.unwrap_err(),
832 TestError("Authentication attempt superseded".to_string())
833 );
834
835 tracker.succeed();
837 let result5: Result<(), TestError> =
838 tracker.wait_for_result(Duration::from_secs(1), rx5).await;
839 assert!(result5.is_ok());
840 }
841
842 #[rstest]
844 #[tokio::test]
845 async fn test_no_sender_leaks() {
846 let tracker = AuthTracker::new();
847
848 for _ in 0..100 {
849 let rx = tracker.begin();
850 let _result: Result<(), TestError> =
851 tracker.wait_for_result(Duration::from_millis(1), rx).await;
852 }
853
854 let rx = tracker.begin();
855 tracker.succeed();
856 let result: Result<(), TestError> =
857 tracker.wait_for_result(Duration::from_secs(1), rx).await;
858 assert!(result.is_ok());
859 }
860
861 #[rstest]
863 #[tokio::test]
864 async fn test_concurrent_succeed_fail_calls() {
865 let tracker = Arc::new(AuthTracker::new());
866 let rx = tracker.begin();
867
868 let mut handles = vec![];
869
870 for _ in 0..50 {
872 let tracker_clone = Arc::clone(&tracker);
873 handles.push(tokio::spawn(async move {
874 tracker_clone.succeed();
875 }));
876 }
877
878 for _ in 0..50 {
880 let tracker_clone = Arc::clone(&tracker);
881 handles.push(tokio::spawn(async move {
882 tracker_clone.fail("Error");
883 }));
884 }
885
886 for handle in handles {
888 handle.await.unwrap();
889 }
890
891 let result: Result<(), TestError> =
893 tracker.wait_for_result(Duration::from_secs(1), rx).await;
894 let _ = result;
896 }
897
898 #[rstest]
899 fn test_is_authenticated_initial_state() {
900 let tracker = AuthTracker::new();
901 assert!(!tracker.is_authenticated());
902 }
903
904 #[rstest]
905 #[tokio::test]
906 async fn test_is_authenticated_after_succeed() {
907 let tracker = AuthTracker::new();
908 assert!(!tracker.is_authenticated());
909
910 let _rx = tracker.begin();
911 assert!(!tracker.is_authenticated());
912
913 tracker.succeed();
914 assert!(tracker.is_authenticated());
915 }
916
917 #[rstest]
918 #[tokio::test]
919 async fn test_is_authenticated_after_fail() {
920 let tracker = AuthTracker::new();
921 let _rx = tracker.begin();
922 tracker.fail("error");
923 assert!(!tracker.is_authenticated());
924 }
925
926 #[rstest]
927 #[tokio::test]
928 async fn test_invalidate_clears_auth_state() {
929 let tracker = AuthTracker::new();
930 let _rx = tracker.begin();
931 tracker.succeed();
932 assert!(tracker.is_authenticated());
933
934 tracker.invalidate();
935 assert!(!tracker.is_authenticated());
936 }
937
938 #[rstest]
939 #[tokio::test]
940 async fn test_begin_clears_auth_state() {
941 let tracker = AuthTracker::new();
942 let _rx1 = tracker.begin();
943 tracker.succeed();
944 assert!(tracker.is_authenticated());
945
946 let _rx2 = tracker.begin();
947 assert!(!tracker.is_authenticated());
948 }
949
950 #[rstest]
951 fn test_is_authenticated_shared_across_clones() {
952 let tracker = AuthTracker::new();
953 let cloned = tracker.clone();
954
955 let _rx = tracker.begin();
956 tracker.succeed();
957
958 assert!(cloned.is_authenticated());
959 }
960
961 #[rstest]
962 fn test_invalidate_shared_across_clones() {
963 let tracker = AuthTracker::new();
964 let cloned = tracker.clone();
965
966 let _rx = tracker.begin();
967 tracker.succeed();
968 assert!(tracker.is_authenticated());
969
970 cloned.invalidate();
971 assert!(!tracker.is_authenticated());
972 }
973
974 #[rstest]
975 fn test_succeed_without_begin_still_updates_auth_state() {
976 let tracker = AuthTracker::new();
977 assert!(!tracker.is_authenticated());
978
979 tracker.succeed();
981 assert!(tracker.is_authenticated());
982 }
983
984 #[rstest]
985 fn test_fail_without_begin_still_updates_auth_state() {
986 let tracker = AuthTracker::new();
987 tracker.succeed();
988 assert!(tracker.is_authenticated());
989
990 tracker.fail("error");
992 assert!(!tracker.is_authenticated());
993 }
994
995 #[rstest]
996 #[tokio::test]
997 async fn test_auth_state_false_after_timeout_until_late_response() {
998 let tracker = AuthTracker::new();
999 let rx = tracker.begin();
1000 assert!(!tracker.is_authenticated());
1001
1002 let result: Result<(), TestError> =
1003 tracker.wait_for_result(Duration::from_millis(10), rx).await;
1004
1005 assert!(result.is_err());
1006 assert!(!tracker.is_authenticated());
1007
1008 tracker.succeed();
1010 assert!(tracker.is_authenticated());
1011 }
1012
1013 #[rstest]
1014 #[tokio::test]
1015 async fn test_wait_for_authenticated_already_authenticated() {
1016 let tracker = AuthTracker::new();
1017 let _rx = tracker.begin();
1018 tracker.succeed();
1019
1020 assert!(
1021 tracker
1022 .wait_for_authenticated(Duration::from_millis(50))
1023 .await
1024 );
1025 }
1026
1027 #[rstest]
1028 #[tokio::test]
1029 async fn test_wait_for_authenticated_succeeds_after_delay() {
1030 let tracker = AuthTracker::new();
1031 let _rx = tracker.begin();
1032
1033 let tracker_clone = tracker.clone();
1034
1035 tokio::spawn(async move {
1036 tokio::time::sleep(Duration::from_millis(50)).await;
1037 tracker_clone.succeed();
1038 });
1039
1040 assert!(tracker.wait_for_authenticated(Duration::from_secs(1)).await);
1041 }
1042
1043 #[rstest]
1044 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1045 async fn test_wait_for_authenticated_no_lost_wakeup_under_race() {
1046 for _ in 0..200 {
1051 let tracker = AuthTracker::new();
1052 let _rx = tracker.begin();
1053
1054 let succeeder = tracker.clone();
1055 let handle = std::thread::spawn(move || succeeder.succeed());
1056
1057 assert!(
1058 tracker
1059 .wait_for_authenticated(Duration::from_millis(500))
1060 .await,
1061 "wakeup lost despite successful authentication"
1062 );
1063 handle.join().unwrap();
1064 }
1065 }
1066
1067 #[rstest]
1068 #[tokio::test]
1069 async fn test_wait_for_authenticated_returns_false_on_failure() {
1070 let tracker = AuthTracker::new();
1071 let _rx = tracker.begin();
1072
1073 let tracker_clone = tracker.clone();
1074
1075 tokio::spawn(async move {
1076 tokio::time::sleep(Duration::from_millis(50)).await;
1077 tracker_clone.fail("rejected");
1078 });
1079
1080 let start = tokio::time::Instant::now();
1081 let result = tracker.wait_for_authenticated(Duration::from_secs(5)).await;
1082 let elapsed = start.elapsed();
1083
1084 assert!(!result);
1085 assert!(elapsed < Duration::from_secs(1));
1086 }
1087
1088 #[rstest]
1089 #[tokio::test]
1090 async fn test_wait_for_authenticated_times_out() {
1091 let tracker = AuthTracker::new();
1092 let _rx = tracker.begin();
1093
1094 assert!(
1095 !tracker
1096 .wait_for_authenticated(Duration::from_millis(50))
1097 .await
1098 );
1099 }
1100
1101 #[rstest]
1102 #[tokio::test]
1103 async fn test_wait_for_authenticated_begin_clears_failed() {
1104 let tracker = AuthTracker::new();
1105 let _rx = tracker.begin();
1106 tracker.fail("first attempt");
1107
1108 assert!(
1109 !tracker
1110 .wait_for_authenticated(Duration::from_millis(10))
1111 .await
1112 );
1113
1114 let _rx = tracker.begin();
1116
1117 let tracker_clone = tracker.clone();
1118
1119 tokio::spawn(async move {
1120 tokio::time::sleep(Duration::from_millis(50)).await;
1121 tracker_clone.succeed();
1122 });
1123
1124 assert!(tracker.wait_for_authenticated(Duration::from_secs(1)).await);
1125 }
1126
1127 #[rstest]
1128 #[tokio::test]
1129 async fn test_wait_for_authenticated_invalidate_does_not_return_false() {
1130 let tracker = AuthTracker::new();
1131 let _rx = tracker.begin();
1132
1133 let tracker_clone = tracker.clone();
1134
1135 tokio::spawn(async move {
1136 tokio::time::sleep(Duration::from_millis(20)).await;
1138 tracker_clone.invalidate();
1139 tokio::time::sleep(Duration::from_millis(20)).await;
1141 tracker_clone.succeed();
1142 });
1143
1144 assert!(tracker.wait_for_authenticated(Duration::from_secs(1)).await);
1145 }
1146
1147 #[rstest]
1148 #[tokio::test]
1149 async fn test_wait_for_authenticated_concurrent_waiters() {
1150 let tracker = Arc::new(AuthTracker::new());
1151 let _rx = tracker.begin();
1152
1153 let mut handles = vec![];
1154
1155 for _ in 0..10 {
1156 let t = Arc::clone(&tracker);
1157 handles.push(tokio::spawn(async move {
1158 t.wait_for_authenticated(Duration::from_secs(1)).await
1159 }));
1160 }
1161
1162 tokio::time::sleep(Duration::from_millis(50)).await;
1163 tracker.succeed();
1164
1165 for handle in handles {
1166 assert!(handle.await.unwrap());
1167 }
1168 }
1169
1170 #[rstest]
1171 #[tokio::test]
1172 async fn test_wait_for_authenticated_not_authenticated_initially() {
1173 let tracker = AuthTracker::new();
1174
1175 assert!(
1178 !tracker
1179 .wait_for_authenticated(Duration::from_millis(50))
1180 .await
1181 );
1182 }
1183}
1184
1185#[cfg(test)]
1186mod proptest_tests {
1187 use std::{sync::Arc, time::Duration};
1188
1189 use proptest::prelude::*;
1190 use rstest::rstest;
1191
1192 use super::*;
1193
1194 const AUTH_FAILED: &str = "model auth failed";
1195 const AUTH_SUPERSEDED: &str = "Authentication attempt superseded";
1196
1197 #[derive(Debug, Clone)]
1198 enum AuthTraceOp {
1199 Begin,
1200 Succeed,
1201 Fail,
1202 Invalidate,
1203 WaitForAuthenticated,
1204 }
1205
1206 #[derive(Debug, Clone, PartialEq, Eq)]
1207 enum ExpectedAuthResult {
1208 Success,
1209 Failed(&'static str),
1210 }
1211
1212 #[derive(Debug)]
1213 struct AuthTraceModel {
1214 state: AuthState,
1215 pending_receiver: Option<usize>,
1216 expected_results: Vec<Option<ExpectedAuthResult>>,
1217 }
1218
1219 impl AuthTraceModel {
1220 fn new() -> Self {
1221 Self {
1222 state: AuthState::Unauthenticated,
1223 pending_receiver: None,
1224 expected_results: Vec::new(),
1225 }
1226 }
1227
1228 fn begin(&mut self) {
1229 if let Some(receiver_index) = self.pending_receiver.take() {
1230 self.expected_results[receiver_index] =
1231 Some(ExpectedAuthResult::Failed(AUTH_SUPERSEDED));
1232 }
1233
1234 self.pending_receiver = Some(self.expected_results.len());
1235 self.expected_results.push(None);
1236 self.state = AuthState::Unauthenticated;
1237 }
1238
1239 fn succeed(&mut self) {
1240 self.state = AuthState::Authenticated;
1241
1242 if let Some(receiver_index) = self.pending_receiver.take() {
1243 self.expected_results[receiver_index] = Some(ExpectedAuthResult::Success);
1244 }
1245 }
1246
1247 fn fail(&mut self) {
1248 self.state = AuthState::Failed;
1249
1250 if let Some(receiver_index) = self.pending_receiver.take() {
1251 self.expected_results[receiver_index] =
1252 Some(ExpectedAuthResult::Failed(AUTH_FAILED));
1253 }
1254 }
1255
1256 fn invalidate(&mut self) {
1257 self.state = AuthState::Unauthenticated;
1258 }
1259 }
1260
1261 fn auth_trace_op_strategy() -> impl Strategy<Value = AuthTraceOp> {
1262 prop_oneof![
1263 Just(AuthTraceOp::Begin),
1264 Just(AuthTraceOp::Succeed),
1265 Just(AuthTraceOp::Fail),
1266 Just(AuthTraceOp::Invalidate),
1267 Just(AuthTraceOp::WaitForAuthenticated),
1268 ]
1269 }
1270
1271 fn assert_auth_receivers_match_model(
1272 receivers: &mut [Option<AuthResultReceiver>],
1273 model: &AuthTraceModel,
1274 step: usize,
1275 ) -> Result<(), TestCaseError> {
1276 for (receiver_index, receiver_slot) in receivers.iter_mut().enumerate() {
1277 let Some(receiver) = receiver_slot.as_mut() else {
1278 continue;
1279 };
1280
1281 let mut clear_receiver = false;
1282
1283 match &model.expected_results[receiver_index] {
1284 Some(ExpectedAuthResult::Success) => {
1285 match receiver.try_recv() {
1286 Ok(Ok(())) => {}
1287 actual => prop_assert!(
1288 false,
1289 "receiver {} should succeed at step {}, was {:?}",
1290 receiver_index,
1291 step,
1292 actual
1293 ),
1294 }
1295 clear_receiver = true;
1296 }
1297 Some(ExpectedAuthResult::Failed(expected)) => {
1298 match receiver.try_recv() {
1299 Ok(Err(actual)) => prop_assert_eq!(
1300 actual,
1301 *expected,
1302 "receiver {} should fail at step {}",
1303 receiver_index,
1304 step
1305 ),
1306 actual => prop_assert!(
1307 false,
1308 "receiver {} should fail at step {}, was {:?}",
1309 receiver_index,
1310 step,
1311 actual
1312 ),
1313 }
1314 clear_receiver = true;
1315 }
1316 None => {
1317 prop_assert_eq!(
1318 receiver.try_recv(),
1319 Err(tokio::sync::oneshot::error::TryRecvError::Empty),
1320 "receiver {} should stay pending at step {}",
1321 receiver_index,
1322 step
1323 );
1324 }
1325 }
1326
1327 if clear_receiver {
1328 *receiver_slot = None;
1329 }
1330 }
1331
1332 Ok(())
1333 }
1334
1335 proptest! {
1336 #![proptest_config(ProptestConfig::with_cases(256))]
1337
1338 #[rstest]
1341 fn test_auth_tracker_trace_matches_cycle_model(
1342 ops in proptest::collection::vec(auth_trace_op_strategy(), 1..80)
1343 ) {
1344 let runtime = tokio::runtime::Builder::new_current_thread()
1345 .enable_time()
1346 .build()
1347 .unwrap();
1348 let tracker = AuthTracker::new();
1349 let mut model = AuthTraceModel::new();
1350 let mut receivers: Vec<Option<AuthResultReceiver>> = Vec::new();
1351
1352 for (step, op) in ops.iter().enumerate() {
1353 match op {
1354 AuthTraceOp::Begin => {
1355 receivers.push(Some(tracker.begin()));
1356 model.begin();
1357 }
1358 AuthTraceOp::Succeed => {
1359 tracker.succeed();
1360 model.succeed();
1361 }
1362 AuthTraceOp::Fail => {
1363 tracker.fail(AUTH_FAILED);
1364 model.fail();
1365 }
1366 AuthTraceOp::Invalidate => {
1367 tracker.invalidate();
1368 model.invalidate();
1369 }
1370 AuthTraceOp::WaitForAuthenticated => {
1371 let actual = runtime
1372 .block_on(tracker.wait_for_authenticated(Duration::from_millis(0)));
1373 let expected = model.state == AuthState::Authenticated;
1374 prop_assert_eq!(
1375 actual,
1376 expected,
1377 "wait_for_authenticated mismatch at step {}, op {:?}",
1378 step,
1379 op
1380 );
1381 }
1382 }
1383
1384 prop_assert_eq!(
1385 tracker.auth_state(),
1386 model.state,
1387 "auth state mismatch at step {}, op {:?}",
1388 step,
1389 op
1390 );
1391 assert_auth_receivers_match_model(&mut receivers, &model, step)?;
1392 }
1393 }
1394
1395 #[rstest]
1399 fn test_state_consistency_after_random_operations(
1400 ops in proptest::collection::vec(0u8..4, 1..50)
1401 ) {
1402 let tracker = AuthTracker::new();
1403 let mut expected_auth = false;
1404
1405 for op in &ops {
1406 match op {
1407 0 => {
1408 let _rx = tracker.begin();
1409 expected_auth = false;
1410 }
1411 1 => {
1412 tracker.succeed();
1413 expected_auth = true;
1414 }
1415 2 => {
1416 tracker.fail("test");
1417 expected_auth = false;
1418 }
1419 3 => {
1420 tracker.invalidate();
1421 expected_auth = false;
1422 }
1423 _ => unreachable!(),
1424 }
1425 }
1426
1427 prop_assert_eq!(tracker.is_authenticated(), expected_auth);
1428 }
1429
1430 #[rstest]
1433 fn test_begin_always_clears_failed(
1434 prior_ops in proptest::collection::vec(0u8..4, 0..20)
1435 ) {
1436 let tracker = AuthTracker::new();
1437
1438 for op in &prior_ops {
1439 match op {
1440 0 => { let _rx = tracker.begin(); }
1441 1 => tracker.succeed(),
1442 2 => tracker.fail("test"),
1443 3 => tracker.invalidate(),
1444 _ => unreachable!(),
1445 }
1446 }
1447
1448 let _rx = tracker.begin();
1449 prop_assert_eq!(tracker.auth_state(), AuthState::Unauthenticated);
1451 }
1452
1453 #[rstest]
1456 fn test_succeed_always_sets_authenticated(
1457 prior_ops in proptest::collection::vec(0u8..4, 0..20)
1458 ) {
1459 let tracker = AuthTracker::new();
1460
1461 for op in &prior_ops {
1462 match op {
1463 0 => { let _rx = tracker.begin(); }
1464 1 => tracker.succeed(),
1465 2 => tracker.fail("test"),
1466 3 => tracker.invalidate(),
1467 _ => unreachable!(),
1468 }
1469 }
1470
1471 tracker.succeed();
1472 prop_assert_eq!(tracker.auth_state(), AuthState::Authenticated);
1473 }
1474 }
1475
1476 #[rstest]
1479 #[tokio::test]
1480 async fn test_wait_responds_within_bounded_time() {
1481 for auth_result in [true, false] {
1482 let tracker = Arc::new(AuthTracker::new());
1483 let _rx = tracker.begin();
1484
1485 let tracker_clone = Arc::clone(&tracker);
1486
1487 tokio::spawn(async move {
1488 tokio::time::sleep(Duration::from_millis(30)).await;
1489
1490 if auth_result {
1491 tracker_clone.succeed();
1492 } else {
1493 tracker_clone.fail("rejected");
1494 }
1495 });
1496
1497 let start = tokio::time::Instant::now();
1498 let result = tracker
1499 .wait_for_authenticated(Duration::from_secs(10))
1500 .await;
1501 let elapsed = start.elapsed();
1502
1503 assert_eq!(result, auth_result);
1504 assert!(
1505 elapsed < Duration::from_millis(500),
1506 "wait_for_authenticated took {elapsed:?} for auth_result={auth_result}"
1507 );
1508 }
1509 }
1510}