1use std::{
32 pin::pin,
33 sync::{
34 Arc,
35 atomic::{AtomicU8, Ordering},
36 },
37 time::Duration,
38};
39
40use parking_lot::Mutex;
41
42use crate::dst;
43
44pub type AuthResultSender = tokio::sync::oneshot::Sender<Result<(), String>>;
45pub type AuthResultReceiver = tokio::sync::oneshot::Receiver<Result<(), String>>;
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49#[repr(u8)]
50pub enum AuthState {
51 #[default]
53 Unauthenticated = 0,
54 Authenticated = 1,
56 Failed = 2,
58}
59
60impl AuthState {
61 #[inline]
62 #[must_use]
63 #[expect(
64 clippy::match_same_arms,
65 reason = "explicit variant listing is clearer than collapsing 0 with wildcard"
66 )]
67 fn from_u8(value: u8) -> Self {
68 match value {
69 0 => Self::Unauthenticated,
70 1 => Self::Authenticated,
71 2 => Self::Failed,
72 _ => Self::Unauthenticated,
73 }
74 }
75
76 #[inline]
77 #[must_use]
78 const fn as_u8(self) -> u8 {
79 self as u8
80 }
81}
82
83#[derive(Clone, Debug)]
109pub struct AuthTracker {
110 tx: Arc<Mutex<Option<AuthResultSender>>>,
111 state: Arc<AtomicU8>,
112 state_notify: Arc<tokio::sync::Notify>,
113}
114
115impl AuthTracker {
116 #[must_use]
118 pub fn new() -> Self {
119 Self {
120 tx: Arc::new(Mutex::new(None)),
121 state: Arc::new(AtomicU8::new(AuthState::Unauthenticated.as_u8())),
122 state_notify: Arc::new(tokio::sync::Notify::new()),
123 }
124 }
125
126 #[must_use]
128 pub fn auth_state(&self) -> AuthState {
129 AuthState::from_u8(self.state.load(Ordering::Acquire))
130 }
131
132 #[must_use]
134 pub fn is_authenticated(&self) -> bool {
135 self.auth_state() == AuthState::Authenticated
136 }
137
138 pub fn invalidate(&self) {
144 let _guard = self.tx.lock();
145
146 if self
147 .state
148 .try_update(Ordering::AcqRel, Ordering::Acquire, |state| {
149 (AuthState::from_u8(state) == AuthState::Authenticated)
150 .then_some(AuthState::Unauthenticated.as_u8())
151 })
152 .is_ok()
153 {
154 self.state_notify.notify_waiters();
155 }
156 }
157
158 #[allow(
167 clippy::must_use_candidate,
168 reason = "callers use this for side effects"
169 )]
170 pub fn begin(&self) -> AuthResultReceiver {
171 let (sender, receiver) = tokio::sync::oneshot::channel();
172 let mut guard = self.tx.lock();
173 self.state
174 .store(AuthState::Unauthenticated.as_u8(), Ordering::Release);
175
176 if let Some(old) = guard.take() {
177 log::warn!("New authentication request superseding previous pending request");
178 let _ = old.send(Err("Authentication attempt superseded".to_string()));
179 } else {
180 log::debug!("Starting new authentication request");
181 }
182
183 *guard = Some(sender);
184
185 receiver
186 }
187
188 pub fn succeed(&self) {
197 let mut guard = self.tx.lock();
198 self.state
199 .store(AuthState::Authenticated.as_u8(), Ordering::Release);
200 self.state_notify.notify_waiters();
201
202 if let Some(sender) = guard.take() {
203 let _ = sender.send(Ok(()));
204 }
205 }
206
207 pub fn fail(&self, error: impl Into<String>) {
216 let message = error.into();
217 let mut guard = self.tx.lock();
218 self.state
219 .store(AuthState::Failed.as_u8(), Ordering::Release);
220 self.state_notify.notify_waiters();
221
222 if let Some(sender) = guard.take() {
223 let _ = sender.send(Err(message));
224 }
225 }
226
227 pub async fn wait_for_result<E>(
244 &self,
245 timeout: Duration,
246 receiver: AuthResultReceiver,
247 ) -> Result<(), E>
248 where
249 E: From<String>,
250 {
251 match dst::time::timeout(timeout, receiver).await {
252 Ok(Ok(Ok(()))) => Ok(()),
253 Ok(Ok(Err(msg))) => Err(E::from(msg)),
254 Ok(Err(_)) => Err(E::from("Authentication channel closed".to_string())),
255 Err(_) => {
256 Err(E::from("Authentication timed out".to_string()))
260 }
261 }
262 }
263
264 pub async fn wait_for_authenticated(&self, timeout: Duration) -> bool {
278 if self.is_authenticated() {
279 return true;
280 }
281
282 dst::time::timeout(timeout, async {
283 loop {
284 let mut notified = pin!(self.state_notify.notified());
286 notified.as_mut().enable();
287
288 match self.auth_state() {
289 AuthState::Authenticated => return true,
290 AuthState::Failed => return false,
291 AuthState::Unauthenticated => notified.await,
292 }
293 }
294 })
295 .await
296 .unwrap_or(false)
297 }
298}
299
300impl Default for AuthTracker {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306#[cfg(test)]
307#[cfg(not(all(feature = "simulation", madsim)))]
308mod tests {
309 use std::{
310 sync::{
311 atomic::{AtomicBool, Ordering},
312 mpsc,
313 },
314 time::Duration,
315 };
316
317 use rstest::rstest;
318
319 use super::*;
320
321 #[derive(Debug, PartialEq)]
322 struct TestError(String);
323
324 impl From<String> for TestError {
325 fn from(msg: String) -> Self {
326 Self(msg)
327 }
328 }
329
330 #[rstest]
331 fn test_begin_during_failure_keeps_receiver_and_state_consistent() {
332 struct DeferredError {
333 entered: mpsc::SyncSender<()>,
334 proceed: mpsc::Receiver<()>,
335 }
336
337 impl From<DeferredError> for String {
338 fn from(error: DeferredError) -> Self {
339 error.entered.send(()).unwrap();
340 error.proceed.recv_timeout(Duration::from_secs(2)).unwrap();
341 "credentials rejected".to_string()
342 }
343 }
344
345 let tracker = AuthTracker::new();
346 let (entered_tx, entered_rx) = mpsc::sync_channel(1);
347 let (proceed_tx, proceed_rx) = mpsc::sync_channel(1);
348
349 let mut receiver = std::thread::scope(|scope| {
350 let completion = scope.spawn(|| {
351 tracker.fail(DeferredError {
352 entered: entered_tx,
353 proceed: proceed_rx,
354 });
355 });
356
357 entered_rx.recv_timeout(Duration::from_secs(2)).unwrap();
358 let receiver = tracker.begin();
359 proceed_tx.send(()).unwrap();
360 completion.join().unwrap();
361 receiver
362 });
363
364 assert_eq!(
365 receiver.try_recv(),
366 Ok(Err("credentials rejected".to_string()))
367 );
368 assert_eq!(tracker.auth_state(), AuthState::Failed);
369 }
370
371 #[rstest]
372 #[case::success(AuthTracker::succeed, Ok(()), AuthState::Authenticated)]
373 #[case::failure(|tracker: &AuthTracker| tracker.fail("rejected"), Err("rejected".to_string()), AuthState::Failed)]
374 fn test_concurrent_begin_and_completion_keep_receiver_and_state_consistent(
375 #[case] complete: fn(&AuthTracker),
376 #[case] result: Result<(), String>,
377 #[case] state: AuthState,
378 ) {
379 const ATTEMPTS: usize = 2_000;
380
381 let tracker = AuthTracker::new();
382 let (begin_tx, begin_rx) = mpsc::sync_channel(1);
383 let (complete_tx, complete_rx) = mpsc::sync_channel(1);
384 let (receiver_tx, receiver_rx) = mpsc::sync_channel(1);
385 let (done_tx, done_rx) = mpsc::sync_channel(1);
386
387 let observations = std::thread::scope(|scope| {
388 let tracker = &tracker;
389
390 scope.spawn(move || {
391 for _ in 0..ATTEMPTS {
392 begin_rx.recv_timeout(Duration::from_secs(2)).unwrap();
393 receiver_tx.send(tracker.begin()).unwrap();
394 }
395 });
396
397 scope.spawn(move || {
398 for _ in 0..ATTEMPTS {
399 complete_rx.recv_timeout(Duration::from_secs(2)).unwrap();
400 complete(tracker);
401 done_tx.send(()).unwrap();
402 }
403 });
404
405 let mut observations = Vec::with_capacity(ATTEMPTS);
406
407 for _ in 0..ATTEMPTS {
408 begin_tx.send(()).unwrap();
409 complete_tx.send(()).unwrap();
410 let mut receiver = receiver_rx.recv_timeout(Duration::from_secs(2)).unwrap();
411 done_rx.recv_timeout(Duration::from_secs(2)).unwrap();
412 observations.push((receiver.try_recv(), tracker.auth_state()));
413 }
414
415 observations
416 });
417
418 for (received, actual_state) in observations {
419 match received {
420 Ok(actual_result) => {
421 assert_eq!(actual_result, result);
422 assert_eq!(actual_state, state);
423 }
424 Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
425 assert_eq!(actual_state, AuthState::Unauthenticated);
426 }
427 Err(receive_error) => panic!("authentication channel closed: {receive_error}"),
428 }
429 }
430 }
431
432 #[rstest]
433 #[tokio::test]
434 async fn test_successful_authentication() {
435 let tracker = AuthTracker::new();
436 let rx = tracker.begin();
437
438 tracker.succeed();
439
440 let result: Result<(), TestError> =
441 tracker.wait_for_result(Duration::from_secs(1), rx).await;
442
443 assert!(result.is_ok());
444 }
445
446 #[rstest]
447 #[tokio::test]
448 async fn test_failed_authentication() {
449 let tracker = AuthTracker::new();
450 let rx = tracker.begin();
451
452 tracker.fail("Invalid credentials");
453
454 let result: Result<(), TestError> =
455 tracker.wait_for_result(Duration::from_secs(1), rx).await;
456
457 assert_eq!(
458 result.unwrap_err(),
459 TestError("Invalid credentials".to_string())
460 );
461 }
462
463 #[rstest]
464 #[tokio::test]
465 async fn test_authentication_timeout() {
466 let tracker = AuthTracker::new();
467 let rx = tracker.begin();
468
469 let result: Result<(), TestError> =
472 tracker.wait_for_result(Duration::from_millis(50), rx).await;
473
474 assert_eq!(
475 result.unwrap_err(),
476 TestError("Authentication timed out".to_string())
477 );
478 }
479
480 #[rstest]
481 #[tokio::test]
482 async fn test_begin_supersedes_previous_sender() {
483 let tracker = AuthTracker::new();
484
485 let first = tracker.begin();
486 let second = tracker.begin();
487
488 let result = first.await.expect("oneshot closed unexpectedly");
490 assert_eq!(result, Err("Authentication attempt superseded".to_string()));
491
492 tracker.succeed();
494 let result: Result<(), TestError> = tracker
495 .wait_for_result(Duration::from_secs(1), second)
496 .await;
497
498 assert!(result.is_ok());
499 }
500
501 #[rstest]
502 #[tokio::test]
503 async fn test_succeed_without_pending_auth() {
504 let tracker = AuthTracker::new();
505
506 tracker.succeed();
507
508 assert_eq!(tracker.auth_state(), AuthState::Authenticated);
509 }
510
511 #[rstest]
512 #[tokio::test]
513 async fn test_fail_without_pending_auth() {
514 let tracker = AuthTracker::new();
515
516 tracker.fail("Some error");
517
518 assert_eq!(tracker.auth_state(), AuthState::Failed);
519 }
520
521 #[rstest]
522 #[tokio::test]
523 async fn test_multiple_sequential_authentications() {
524 let tracker = AuthTracker::new();
525
526 let rx1 = tracker.begin();
528 tracker.succeed();
529 let result1: Result<(), TestError> =
530 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
531 assert!(result1.is_ok());
532
533 let rx2 = tracker.begin();
535 tracker.fail("Credentials expired");
536 let result2: Result<(), TestError> =
537 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
538 assert_eq!(
539 result2.unwrap_err(),
540 TestError("Credentials expired".to_string())
541 );
542
543 let rx3 = tracker.begin();
545 tracker.succeed();
546 let result3: Result<(), TestError> =
547 tracker.wait_for_result(Duration::from_secs(1), rx3).await;
548 assert!(result3.is_ok());
549 }
550
551 #[rstest]
552 #[tokio::test]
553 async fn test_channel_closed_before_result() {
554 let tracker = AuthTracker::new();
555 let rx = tracker.begin();
556
557 drop(tracker.tx.lock().take());
558
559 let result: Result<(), TestError> =
560 tracker.wait_for_result(Duration::from_secs(1), rx).await;
561
562 assert_eq!(
563 result,
564 Err(TestError("Authentication channel closed".to_string()))
565 );
566 assert_eq!(tracker.auth_state(), AuthState::Unauthenticated);
567 }
568
569 #[rstest]
570 #[tokio::test]
571 async fn test_concurrent_auth_attempts() {
572 let tracker = Arc::new(AuthTracker::new());
573 let mut handles = vec![];
574
575 for i in 0..10 {
577 let tracker_clone = Arc::clone(&tracker);
578 let handle = tokio::spawn(async move {
579 let rx = tracker_clone.begin();
580
581 if i == 9 {
583 tokio::time::sleep(Duration::from_millis(10)).await;
584 tracker_clone.succeed();
585 }
586
587 let result: Result<(), TestError> = tracker_clone
588 .wait_for_result(Duration::from_secs(1), rx)
589 .await;
590
591 (i, result)
592 });
593
594 handles.push(handle);
595 }
596
597 let mut successes = 0;
598 let mut superseded = 0;
599
600 for handle in handles {
601 let (i, result) = handle.await.unwrap();
602 match result {
603 Ok(()) => {
604 assert_eq!(i, 9);
606 successes += 1;
607 }
608 Err(TestError(msg)) if msg.contains("superseded") => {
609 superseded += 1;
610 }
611 Err(e) => panic!("Unexpected error: {e:?}"),
612 }
613 }
614
615 assert_eq!(successes, 1);
616 assert_eq!(superseded, 9);
617 }
618
619 #[rstest]
620 fn test_default_trait() {
621 let _tracker = AuthTracker::default();
622 }
623
624 #[rstest]
625 #[tokio::test]
626 async fn test_clone_trait() {
627 let tracker = AuthTracker::new();
628 let cloned = tracker.clone();
629
630 let rx = tracker.begin();
632 cloned.succeed(); let result: Result<(), TestError> =
634 tracker.wait_for_result(Duration::from_secs(1), rx).await;
635 assert!(result.is_ok());
636 }
637
638 #[rstest]
639 fn test_debug_trait() {
640 let tracker = AuthTracker::new();
641 let debug_str = format!("{tracker:?}");
642 assert!(debug_str.contains("AuthTracker"));
643 }
644
645 #[rstest]
646 #[tokio::test]
647 async fn test_timeout_clears_sender() {
648 let tracker = AuthTracker::new();
649
650 let rx1 = tracker.begin();
652 let result1: Result<(), TestError> = tracker
653 .wait_for_result(Duration::from_millis(50), rx1)
654 .await;
655 assert_eq!(
656 result1.unwrap_err(),
657 TestError("Authentication timed out".to_string())
658 );
659
660 let rx2 = tracker.begin();
662 tracker.succeed();
663 let result2: Result<(), TestError> =
664 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
665 assert!(result2.is_ok());
666 }
667
668 #[rstest]
669 #[tokio::test]
670 async fn test_fail_clears_sender() {
671 let tracker = AuthTracker::new();
672
673 let rx1 = tracker.begin();
675 tracker.fail("Bad credentials");
676 let result1: Result<(), TestError> =
677 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
678 assert!(result1.is_err());
679
680 let rx2 = tracker.begin();
682 tracker.succeed();
683 let result2: Result<(), TestError> =
684 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
685 assert!(result2.is_ok());
686 }
687
688 #[rstest]
689 #[tokio::test]
690 async fn test_succeed_clears_sender() {
691 let tracker = AuthTracker::new();
692
693 let rx1 = tracker.begin();
695 tracker.succeed();
696 let result1: Result<(), TestError> =
697 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
698 assert!(result1.is_ok());
699
700 let rx2 = tracker.begin();
702 tracker.succeed();
703 let result2: Result<(), TestError> =
704 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
705 assert!(result2.is_ok());
706 }
707
708 #[rstest]
709 #[tokio::test]
710 async fn test_rapid_begin_succeed_cycles() {
711 let tracker = AuthTracker::new();
712
713 for _ in 0..100 {
715 let rx = tracker.begin();
716 tracker.succeed();
717 let result: Result<(), TestError> =
718 tracker.wait_for_result(Duration::from_secs(1), rx).await;
719 assert!(result.is_ok());
720 }
721 }
722
723 #[rstest]
724 #[tokio::test]
725 async fn test_double_succeed_is_safe() {
726 let tracker = AuthTracker::new();
727 let rx = tracker.begin();
728
729 tracker.succeed();
731 tracker.succeed(); let result: Result<(), TestError> =
734 tracker.wait_for_result(Duration::from_secs(1), rx).await;
735 assert!(result.is_ok());
736 }
737
738 #[rstest]
739 #[tokio::test]
740 async fn test_double_fail_is_safe() {
741 let tracker = AuthTracker::new();
742 let rx = tracker.begin();
743
744 tracker.fail("Error 1");
746 tracker.fail("Error 2"); let result: Result<(), TestError> =
749 tracker.wait_for_result(Duration::from_secs(1), rx).await;
750 assert_eq!(
751 result.unwrap_err(),
752 TestError("Error 1".to_string()) );
754 }
755
756 #[rstest]
757 #[tokio::test]
758 async fn test_succeed_after_fail_is_ignored() {
759 let tracker = AuthTracker::new();
760 let rx = tracker.begin();
761
762 tracker.fail("Auth failed");
763 tracker.succeed(); let result: Result<(), TestError> =
766 tracker.wait_for_result(Duration::from_secs(1), rx).await;
767 assert!(result.is_err()); }
769
770 #[rstest]
771 #[tokio::test]
772 async fn test_fail_after_succeed_is_ignored() {
773 let tracker = AuthTracker::new();
774 let rx = tracker.begin();
775
776 tracker.succeed();
777 tracker.fail("Auth failed"); let result: Result<(), TestError> =
780 tracker.wait_for_result(Duration::from_secs(1), rx).await;
781 assert!(result.is_ok()); }
783
784 #[rstest]
791 #[tokio::test]
792 async fn test_reconnect_flow_waits_for_auth() {
793 let tracker = Arc::new(AuthTracker::new());
794 let subscribed = Arc::new(tokio::sync::Notify::new());
795 let auth_completed = Arc::new(tokio::sync::Notify::new());
796
797 let tracker_reconnect = Arc::clone(&tracker);
799 let subscribed_reconnect = Arc::clone(&subscribed);
800 let auth_completed_reconnect = Arc::clone(&auth_completed);
801
802 let reconnect_task = tokio::spawn(async move {
803 let rx = tracker_reconnect.begin();
805
806 let tracker_resub = Arc::clone(&tracker_reconnect);
808 let subscribed_resub = Arc::clone(&subscribed_reconnect);
809 let auth_completed_resub = Arc::clone(&auth_completed_reconnect);
810
811 let resub_task = tokio::spawn(async move {
812 let result: Result<(), TestError> = tracker_resub
814 .wait_for_result(Duration::from_secs(5), rx)
815 .await;
816
817 if result.is_ok() {
818 auth_completed_resub.notify_one();
819 tokio::time::sleep(Duration::from_millis(10)).await;
821 subscribed_resub.notify_one();
822 }
823 });
824
825 resub_task.await.unwrap();
826 });
827
828 tokio::time::sleep(Duration::from_millis(100)).await;
830 tracker.succeed();
831
832 reconnect_task.await.unwrap();
834
835 tokio::select! {
837 () = auth_completed.notified() => {
838 }
840 () = tokio::time::sleep(Duration::from_secs(1)) => {
841 panic!("Auth never completed");
842 }
843 }
844
845 tokio::select! {
847 () = subscribed.notified() => {
848 }
850 () = tokio::time::sleep(Duration::from_secs(1)) => {
851 panic!("Subscription never completed");
852 }
853 }
854 }
855
856 #[rstest]
858 #[tokio::test]
859 async fn test_reconnect_flow_blocks_on_auth_failure() {
860 let tracker = Arc::new(AuthTracker::new());
861 let subscribed = Arc::new(AtomicBool::new(false));
862
863 let tracker_reconnect = Arc::clone(&tracker);
864 let subscribed_reconnect = Arc::clone(&subscribed);
865
866 let reconnect_task = tokio::spawn(async move {
867 let rx = tracker_reconnect.begin();
868
869 let tracker_resub = Arc::clone(&tracker_reconnect);
871 let subscribed_resub = Arc::clone(&subscribed_reconnect);
872
873 let resub_task = tokio::spawn(async move {
874 let result: Result<(), TestError> = tracker_resub
875 .wait_for_result(Duration::from_secs(5), rx)
876 .await;
877
878 if result.is_ok() {
880 subscribed_resub.store(true, Ordering::Relaxed);
881 }
882 });
883
884 resub_task.await.unwrap();
885 });
886
887 tokio::time::sleep(Duration::from_millis(50)).await;
889 tracker.fail("Invalid credentials");
890
891 reconnect_task.await.unwrap();
893
894 tokio::time::sleep(Duration::from_millis(100)).await;
896 assert!(!subscribed.load(Ordering::Relaxed));
897 }
898
899 #[rstest]
901 #[tokio::test]
902 async fn test_state_machine_transitions() {
903 let tracker = AuthTracker::new();
904
905 let rx1 = tracker.begin();
907
908 tracker.succeed();
910 let result1: Result<(), TestError> =
911 tracker.wait_for_result(Duration::from_secs(1), rx1).await;
912 assert!(result1.is_ok());
913
914 let rx2 = tracker.begin();
916
917 tracker.fail("Error");
919 let result2: Result<(), TestError> =
920 tracker.wait_for_result(Duration::from_secs(1), rx2).await;
921 assert!(result2.is_err());
922
923 let rx3 = tracker.begin();
925
926 let result3: Result<(), TestError> = tracker
928 .wait_for_result(Duration::from_millis(50), rx3)
929 .await;
930 assert_eq!(
931 result3.unwrap_err(),
932 TestError("Authentication timed out".to_string())
933 );
934
935 let rx4 = tracker.begin();
937
938 let rx5 = tracker.begin();
940 let result4: Result<(), TestError> =
941 tracker.wait_for_result(Duration::from_secs(1), rx4).await;
942 assert_eq!(
943 result4.unwrap_err(),
944 TestError("Authentication attempt superseded".to_string())
945 );
946
947 tracker.succeed();
949 let result5: Result<(), TestError> =
950 tracker.wait_for_result(Duration::from_secs(1), rx5).await;
951 assert!(result5.is_ok());
952 }
953
954 #[rstest]
956 #[tokio::test]
957 async fn test_no_sender_leaks() {
958 let tracker = AuthTracker::new();
959 for _ in 0..100 {
960 let rx = tracker.begin();
961 let _result: Result<(), TestError> =
962 tracker.wait_for_result(Duration::from_millis(1), rx).await;
963 }
964
965 let rx = tracker.begin();
966 tracker.succeed();
967 let result: Result<(), TestError> =
968 tracker.wait_for_result(Duration::from_secs(1), rx).await;
969 assert!(result.is_ok());
970 }
971
972 #[rstest]
974 #[tokio::test]
975 async fn test_concurrent_succeed_fail_calls() {
976 let tracker = Arc::new(AuthTracker::new());
977 let rx = tracker.begin();
978
979 let mut handles = vec![];
980
981 for _ in 0..50 {
983 let tracker_clone = Arc::clone(&tracker);
984 handles.push(tokio::spawn(async move {
985 tracker_clone.succeed();
986 }));
987 }
988
989 for _ in 0..50 {
991 let tracker_clone = Arc::clone(&tracker);
992 handles.push(tokio::spawn(async move {
993 tracker_clone.fail("Error");
994 }));
995 }
996
997 for handle in handles {
999 handle.await.unwrap();
1000 }
1001
1002 let result: Result<(), TestError> =
1004 tracker.wait_for_result(Duration::from_secs(1), rx).await;
1005 let _ = result;
1007 }
1008
1009 #[rstest]
1010 fn test_is_authenticated_initial_state() {
1011 let tracker = AuthTracker::new();
1012 assert!(!tracker.is_authenticated());
1013 }
1014
1015 #[rstest]
1016 #[tokio::test]
1017 async fn test_is_authenticated_after_succeed() {
1018 let tracker = AuthTracker::new();
1019 assert!(!tracker.is_authenticated());
1020
1021 let _rx = tracker.begin();
1022 assert!(!tracker.is_authenticated());
1023
1024 tracker.succeed();
1025 assert!(tracker.is_authenticated());
1026 }
1027
1028 #[rstest]
1029 #[tokio::test]
1030 async fn test_is_authenticated_after_fail() {
1031 let tracker = AuthTracker::new();
1032 let _rx = tracker.begin();
1033 tracker.fail("error");
1034 assert!(!tracker.is_authenticated());
1035 }
1036
1037 #[rstest]
1038 #[tokio::test]
1039 async fn test_invalidate_clears_auth_state() {
1040 let tracker = AuthTracker::new();
1041 let _rx = tracker.begin();
1042 tracker.succeed();
1043 assert!(tracker.is_authenticated());
1044
1045 tracker.invalidate();
1046 assert!(!tracker.is_authenticated());
1047 }
1048
1049 #[rstest]
1050 #[case(true)]
1051 #[case(false)]
1052 #[tokio::test]
1053 async fn test_invalidate_preserves_terminal_failure(#[case] invalidate_first: bool) {
1054 let tracker = AuthTracker::new();
1055 let receiver = tracker.begin();
1056 if invalidate_first {
1057 tracker.invalidate();
1058 tracker.fail("terminal");
1059 } else {
1060 tracker.fail("terminal");
1061 tracker.invalidate();
1062 }
1063
1064 let result: Result<(), TestError> = tracker
1065 .wait_for_result(Duration::from_secs(1), receiver)
1066 .await;
1067
1068 assert_eq!(tracker.auth_state(), AuthState::Failed);
1069 assert_eq!(result.unwrap_err(), TestError("terminal".to_string()));
1070 assert!(
1071 !tracker
1072 .wait_for_authenticated(Duration::from_millis(10))
1073 .await
1074 );
1075 }
1076
1077 #[rstest]
1078 #[tokio::test]
1079 async fn test_begin_clears_auth_state() {
1080 let tracker = AuthTracker::new();
1081 let _rx1 = tracker.begin();
1082 tracker.succeed();
1083 assert!(tracker.is_authenticated());
1084
1085 let _rx2 = tracker.begin();
1086 assert!(!tracker.is_authenticated());
1087 }
1088
1089 #[rstest]
1090 fn test_is_authenticated_shared_across_clones() {
1091 let tracker = AuthTracker::new();
1092 let cloned = tracker.clone();
1093
1094 let _rx = tracker.begin();
1095 tracker.succeed();
1096
1097 assert!(cloned.is_authenticated());
1098 }
1099
1100 #[rstest]
1101 fn test_invalidate_shared_across_clones() {
1102 let tracker = AuthTracker::new();
1103 let cloned = tracker.clone();
1104
1105 let _rx = tracker.begin();
1106 tracker.succeed();
1107 assert!(tracker.is_authenticated());
1108
1109 cloned.invalidate();
1110 assert!(!tracker.is_authenticated());
1111 }
1112
1113 #[rstest]
1114 fn test_succeed_without_begin_still_updates_auth_state() {
1115 let tracker = AuthTracker::new();
1116 assert!(!tracker.is_authenticated());
1117
1118 tracker.succeed();
1120 assert!(tracker.is_authenticated());
1121 }
1122
1123 #[rstest]
1124 fn test_fail_without_begin_still_updates_auth_state() {
1125 let tracker = AuthTracker::new();
1126 tracker.succeed();
1127 assert!(tracker.is_authenticated());
1128
1129 tracker.fail("error");
1131 assert!(!tracker.is_authenticated());
1132 }
1133
1134 #[rstest]
1135 #[tokio::test]
1136 async fn test_auth_state_false_after_timeout_until_late_response() {
1137 let tracker = AuthTracker::new();
1138 let rx = tracker.begin();
1139 assert!(!tracker.is_authenticated());
1140
1141 let result: Result<(), TestError> =
1142 tracker.wait_for_result(Duration::from_millis(10), rx).await;
1143
1144 assert!(result.is_err());
1145 assert!(!tracker.is_authenticated());
1146
1147 tracker.succeed();
1149 assert!(tracker.is_authenticated());
1150 }
1151
1152 #[rstest]
1153 #[tokio::test]
1154 async fn test_wait_for_authenticated_already_authenticated() {
1155 let tracker = AuthTracker::new();
1156 let _rx = tracker.begin();
1157 tracker.succeed();
1158
1159 assert!(
1160 tracker
1161 .wait_for_authenticated(Duration::from_millis(50))
1162 .await
1163 );
1164 }
1165
1166 #[rstest]
1167 #[tokio::test]
1168 async fn test_wait_for_authenticated_succeeds_after_delay() {
1169 let tracker = AuthTracker::new();
1170 let _rx = tracker.begin();
1171
1172 let tracker_clone = tracker.clone();
1173
1174 tokio::spawn(async move {
1175 tokio::time::sleep(Duration::from_millis(50)).await;
1176 tracker_clone.succeed();
1177 });
1178
1179 assert!(tracker.wait_for_authenticated(Duration::from_secs(1)).await);
1180 }
1181
1182 #[rstest]
1183 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1184 async fn test_wait_for_authenticated_no_lost_wakeup_under_race() {
1185 for _ in 0..200 {
1190 let tracker = AuthTracker::new();
1191 let _rx = tracker.begin();
1192
1193 let succeeder = tracker.clone();
1194 let handle = std::thread::spawn(move || succeeder.succeed());
1195
1196 assert!(
1197 tracker
1198 .wait_for_authenticated(Duration::from_millis(500))
1199 .await,
1200 "wakeup lost despite successful authentication"
1201 );
1202 handle.join().unwrap();
1203 }
1204 }
1205
1206 #[rstest]
1207 #[tokio::test]
1208 async fn test_wait_for_authenticated_returns_false_on_failure() {
1209 let tracker = AuthTracker::new();
1210 let _rx = tracker.begin();
1211
1212 let tracker_clone = tracker.clone();
1213
1214 tokio::spawn(async move {
1215 tokio::time::sleep(Duration::from_millis(50)).await;
1216 tracker_clone.fail("rejected");
1217 });
1218
1219 let start = tokio::time::Instant::now();
1220 let result = tracker.wait_for_authenticated(Duration::from_secs(5)).await;
1221 let elapsed = start.elapsed();
1222
1223 assert!(!result);
1224 assert!(elapsed < Duration::from_secs(1));
1225 }
1226
1227 #[rstest]
1228 #[tokio::test]
1229 async fn test_wait_for_authenticated_times_out() {
1230 let tracker = AuthTracker::new();
1231 let _rx = tracker.begin();
1232
1233 assert!(
1234 !tracker
1235 .wait_for_authenticated(Duration::from_millis(50))
1236 .await
1237 );
1238 }
1239
1240 #[rstest]
1241 #[tokio::test]
1242 async fn test_wait_for_authenticated_begin_clears_failed() {
1243 let tracker = AuthTracker::new();
1244 let _rx = tracker.begin();
1245 tracker.fail("first attempt");
1246
1247 assert!(
1248 !tracker
1249 .wait_for_authenticated(Duration::from_millis(10))
1250 .await
1251 );
1252
1253 let _rx = tracker.begin();
1255
1256 let tracker_clone = tracker.clone();
1257
1258 tokio::spawn(async move {
1259 tokio::time::sleep(Duration::from_millis(50)).await;
1260 tracker_clone.succeed();
1261 });
1262
1263 assert!(tracker.wait_for_authenticated(Duration::from_secs(1)).await);
1264 }
1265
1266 #[rstest]
1267 #[tokio::test]
1268 async fn test_wait_for_authenticated_invalidate_does_not_return_false() {
1269 let tracker = AuthTracker::new();
1270 let _rx = tracker.begin();
1271
1272 let tracker_clone = tracker.clone();
1273
1274 tokio::spawn(async move {
1275 tokio::time::sleep(Duration::from_millis(20)).await;
1277 tracker_clone.invalidate();
1278 tokio::time::sleep(Duration::from_millis(20)).await;
1280 tracker_clone.succeed();
1281 });
1282
1283 assert!(tracker.wait_for_authenticated(Duration::from_secs(1)).await);
1284 }
1285
1286 #[rstest]
1287 #[tokio::test]
1288 async fn test_wait_for_authenticated_concurrent_waiters() {
1289 let tracker = Arc::new(AuthTracker::new());
1290 let _rx = tracker.begin();
1291
1292 let mut handles = vec![];
1293
1294 for _ in 0..10 {
1295 let t = Arc::clone(&tracker);
1296 handles.push(tokio::spawn(async move {
1297 t.wait_for_authenticated(Duration::from_secs(1)).await
1298 }));
1299 }
1300
1301 tokio::time::sleep(Duration::from_millis(50)).await;
1302 tracker.succeed();
1303
1304 for handle in handles {
1305 assert!(handle.await.unwrap());
1306 }
1307 }
1308
1309 #[rstest]
1310 #[tokio::test]
1311 async fn test_wait_for_authenticated_not_authenticated_initially() {
1312 let tracker = AuthTracker::new();
1313
1314 assert!(
1317 !tracker
1318 .wait_for_authenticated(Duration::from_millis(50))
1319 .await
1320 );
1321 }
1322}
1323
1324#[cfg(test)]
1325#[cfg(not(all(feature = "simulation", madsim)))]
1326mod proptest_tests {
1327 use std::{sync::Arc, time::Duration};
1328
1329 use proptest::prelude::*;
1330 use rstest::rstest;
1331
1332 use super::*;
1333
1334 const AUTH_FAILED: &str = "model auth failed";
1335 const AUTH_SUPERSEDED: &str = "Authentication attempt superseded";
1336
1337 #[derive(Debug, Clone)]
1338 enum AuthTraceOp {
1339 Begin,
1340 Succeed,
1341 Fail,
1342 Invalidate,
1343 WaitForAuthenticated,
1344 }
1345
1346 #[derive(Debug, Clone, PartialEq, Eq)]
1347 enum ExpectedAuthResult {
1348 Success,
1349 Failed(&'static str),
1350 }
1351
1352 #[derive(Debug)]
1353 struct AuthTraceModel {
1354 state: AuthState,
1355 pending_receiver: Option<usize>,
1356 expected_results: Vec<Option<ExpectedAuthResult>>,
1357 }
1358
1359 impl AuthTraceModel {
1360 fn new() -> Self {
1361 Self {
1362 state: AuthState::Unauthenticated,
1363 pending_receiver: None,
1364 expected_results: Vec::new(),
1365 }
1366 }
1367
1368 fn begin(&mut self) {
1369 if let Some(receiver_index) = self.pending_receiver.take() {
1370 self.expected_results[receiver_index] =
1371 Some(ExpectedAuthResult::Failed(AUTH_SUPERSEDED));
1372 }
1373
1374 self.pending_receiver = Some(self.expected_results.len());
1375 self.expected_results.push(None);
1376 self.state = AuthState::Unauthenticated;
1377 }
1378
1379 fn succeed(&mut self) {
1380 self.state = AuthState::Authenticated;
1381
1382 if let Some(receiver_index) = self.pending_receiver.take() {
1383 self.expected_results[receiver_index] = Some(ExpectedAuthResult::Success);
1384 }
1385 }
1386
1387 fn fail(&mut self) {
1388 self.state = AuthState::Failed;
1389
1390 if let Some(receiver_index) = self.pending_receiver.take() {
1391 self.expected_results[receiver_index] =
1392 Some(ExpectedAuthResult::Failed(AUTH_FAILED));
1393 }
1394 }
1395
1396 fn invalidate(&mut self) {
1397 if self.state == AuthState::Authenticated {
1398 self.state = AuthState::Unauthenticated;
1399 }
1400 }
1401 }
1402
1403 fn auth_trace_op_strategy() -> impl Strategy<Value = AuthTraceOp> {
1404 prop_oneof![
1405 Just(AuthTraceOp::Begin),
1406 Just(AuthTraceOp::Succeed),
1407 Just(AuthTraceOp::Fail),
1408 Just(AuthTraceOp::Invalidate),
1409 Just(AuthTraceOp::WaitForAuthenticated),
1410 ]
1411 }
1412
1413 fn assert_auth_receivers_match_model(
1414 receivers: &mut [Option<AuthResultReceiver>],
1415 model: &AuthTraceModel,
1416 step: usize,
1417 ) -> Result<(), TestCaseError> {
1418 for (receiver_index, receiver_slot) in receivers.iter_mut().enumerate() {
1419 let Some(receiver) = receiver_slot.as_mut() else {
1420 continue;
1421 };
1422
1423 let mut clear_receiver = false;
1424
1425 match &model.expected_results[receiver_index] {
1426 Some(ExpectedAuthResult::Success) => {
1427 match receiver.try_recv() {
1428 Ok(Ok(())) => {}
1429 actual => prop_assert!(
1430 false,
1431 "receiver {} should succeed at step {}, was {:?}",
1432 receiver_index,
1433 step,
1434 actual
1435 ),
1436 }
1437
1438 clear_receiver = true;
1439 }
1440 Some(ExpectedAuthResult::Failed(expected)) => {
1441 match receiver.try_recv() {
1442 Ok(Err(actual)) => prop_assert_eq!(
1443 actual,
1444 *expected,
1445 "receiver {} should fail at step {}",
1446 receiver_index,
1447 step
1448 ),
1449 actual => prop_assert!(
1450 false,
1451 "receiver {} should fail at step {}, was {:?}",
1452 receiver_index,
1453 step,
1454 actual
1455 ),
1456 }
1457
1458 clear_receiver = true;
1459 }
1460 None => {
1461 prop_assert_eq!(
1462 receiver.try_recv(),
1463 Err(tokio::sync::oneshot::error::TryRecvError::Empty),
1464 "receiver {} should stay pending at step {}",
1465 receiver_index,
1466 step
1467 );
1468 }
1469 }
1470
1471 if clear_receiver {
1472 *receiver_slot = None;
1473 }
1474 }
1475
1476 Ok(())
1477 }
1478
1479 proptest! {
1480 #![proptest_config(ProptestConfig::with_cases(256))]
1481
1482 #[rstest]
1485 fn test_auth_tracker_trace_matches_cycle_model(
1486 ops in proptest::collection::vec(auth_trace_op_strategy(), 1..80)
1487 ) {
1488 let runtime = tokio::runtime::Builder::new_current_thread()
1489 .enable_time()
1490 .build()
1491 .unwrap();
1492 let tracker = AuthTracker::new();
1493 let mut model = AuthTraceModel::new();
1494 let mut receivers: Vec<Option<AuthResultReceiver>> = Vec::new();
1495
1496 for (step, op) in ops.iter().enumerate() {
1497 match op {
1498 AuthTraceOp::Begin => {
1499 receivers.push(Some(tracker.begin()));
1500 model.begin();
1501 }
1502 AuthTraceOp::Succeed => {
1503 tracker.succeed();
1504 model.succeed();
1505 }
1506 AuthTraceOp::Fail => {
1507 tracker.fail(AUTH_FAILED);
1508 model.fail();
1509 }
1510 AuthTraceOp::Invalidate => {
1511 tracker.invalidate();
1512 model.invalidate();
1513 }
1514 AuthTraceOp::WaitForAuthenticated => {
1515 let actual = runtime
1516 .block_on(tracker.wait_for_authenticated(Duration::from_millis(0)));
1517 let expected = model.state == AuthState::Authenticated;
1518 prop_assert_eq!(
1519 actual,
1520 expected,
1521 "wait_for_authenticated mismatch at step {}, op {:?}",
1522 step,
1523 op
1524 );
1525 }
1526 }
1527
1528 prop_assert_eq!(
1529 tracker.auth_state(),
1530 model.state,
1531 "auth state mismatch at step {}, op {:?}",
1532 step,
1533 op
1534 );
1535 assert_auth_receivers_match_model(&mut receivers, &model, step)?;
1536 }
1537 }
1538
1539 #[rstest]
1543 fn test_state_consistency_after_random_operations(
1544 ops in proptest::collection::vec(0u8..4, 1..50)
1545 ) {
1546 let tracker = AuthTracker::new();
1547 let mut expected_auth = false;
1548
1549 for op in &ops {
1550 match op {
1551 0 => {
1552 let _rx = tracker.begin();
1553 expected_auth = false;
1554 }
1555 1 => {
1556 tracker.succeed();
1557 expected_auth = true;
1558 }
1559 2 => {
1560 tracker.fail("test");
1561 expected_auth = false;
1562 }
1563 3 => {
1564 tracker.invalidate();
1565 expected_auth = false;
1566 }
1567 _ => unreachable!(),
1568 }
1569 }
1570
1571 prop_assert_eq!(tracker.is_authenticated(), expected_auth);
1572 }
1573
1574 #[rstest]
1577 fn test_begin_always_clears_failed(
1578 prior_ops in proptest::collection::vec(0u8..4, 0..20)
1579 ) {
1580 let tracker = AuthTracker::new();
1581
1582 for op in &prior_ops {
1583 match op {
1584 0 => { let _rx = tracker.begin(); }
1585 1 => tracker.succeed(),
1586 2 => tracker.fail("test"),
1587 3 => tracker.invalidate(),
1588 _ => unreachable!(),
1589 }
1590 }
1591
1592 let _rx = tracker.begin();
1593 prop_assert_eq!(tracker.auth_state(), AuthState::Unauthenticated);
1595 }
1596
1597 #[rstest]
1600 fn test_succeed_always_sets_authenticated(
1601 prior_ops in proptest::collection::vec(0u8..4, 0..20)
1602 ) {
1603 let tracker = AuthTracker::new();
1604
1605 for op in &prior_ops {
1606 match op {
1607 0 => { let _rx = tracker.begin(); }
1608 1 => tracker.succeed(),
1609 2 => tracker.fail("test"),
1610 3 => tracker.invalidate(),
1611 _ => unreachable!(),
1612 }
1613 }
1614
1615 tracker.succeed();
1616 prop_assert_eq!(tracker.auth_state(), AuthState::Authenticated);
1617 }
1618 }
1619
1620 #[rstest]
1623 #[tokio::test]
1624 async fn test_wait_responds_within_bounded_time() {
1625 for auth_result in [true, false] {
1626 let tracker = Arc::new(AuthTracker::new());
1627 let _rx = tracker.begin();
1628
1629 let tracker_clone = Arc::clone(&tracker);
1630
1631 tokio::spawn(async move {
1632 tokio::time::sleep(Duration::from_millis(30)).await;
1633
1634 if auth_result {
1635 tracker_clone.succeed();
1636 } else {
1637 tracker_clone.fail("rejected");
1638 }
1639 });
1640
1641 let start = tokio::time::Instant::now();
1642 let result = tracker
1643 .wait_for_authenticated(Duration::from_secs(10))
1644 .await;
1645 let elapsed = start.elapsed();
1646
1647 assert_eq!(result, auth_result);
1648 assert!(
1649 elapsed < Duration::from_millis(500),
1650 "wait_for_authenticated took {elapsed:?} for auth_result={auth_result}"
1651 );
1652 }
1653 }
1654}
1655
1656#[cfg(all(test, feature = "simulation", madsim))]
1657mod simulation_tests {
1658 use std::time::Duration;
1659
1660 use super::*;
1661
1662 #[madsim::test]
1663 async fn test_wait_for_result_succeeds_without_tokio_reactor() {
1664 let tracker = AuthTracker::new();
1665 let rx = tracker.begin();
1666 tracker.succeed();
1667 let result: Result<(), String> = tracker.wait_for_result(Duration::from_secs(1), rx).await;
1668 assert_eq!(result, Ok(()));
1669 }
1670
1671 #[madsim::test]
1672 async fn test_wait_for_result_times_out_on_virtual_clock() {
1673 let tracker = AuthTracker::new();
1674 let rx = tracker.begin();
1675 let result: Result<(), String> =
1676 tracker.wait_for_result(Duration::from_millis(10), rx).await;
1677 assert_eq!(result, Err("Authentication timed out".to_string()));
1678 }
1679
1680 #[madsim::test]
1681 async fn test_wait_for_authenticated_succeeds_without_tokio_reactor() {
1682 let tracker = AuthTracker::new();
1683 let pending = tracker.clone();
1684
1685 madsim::task::spawn(async move {
1686 madsim::time::sleep(Duration::from_millis(1)).await;
1687 pending.succeed();
1688 });
1689
1690 assert!(tracker.wait_for_authenticated(Duration::from_secs(1)).await);
1691 }
1692}