nautilus_network/websocket/
auth.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Authentication state tracking for WebSocket clients.
17//!
18//! This module provides a robust authentication tracker that coordinates login attempts
19//! and ensures each attempt produces a fresh success or failure signal before operations
20//! resume. It follows a proven pattern used in production.
21//!
22//! # Key Features
23//!
24//! - **Oneshot signaling**: Each auth attempt gets a dedicated channel for result notification.
25//! - **Superseding logic**: New authentication requests cancel pending ones.
26//! - **Timeout handling**: Configurable timeout for authentication responses.
27//! - **Generic error mapping**: Adapters can map to their specific error types.
28//!
29//! # Recommended Integration Pattern
30//!
31//! Based on production usage, the recommended pattern is:
32//!
33//! 1. **Authentication guard**: Maintain `Arc<AtomicBool>` to track auth state separately from tracker.
34//! 2. **Guard checks**: Check guard before all private operations (orders, cancels, etc.).
35//! 3. **Reconnection flow**: Authenticate BEFORE resubscribing to topics.
36//! 4. **Event propagation**: Send auth failures through event channels to consumers.
37//! 5. **State lifecycle**: Clear guard on disconnect, set on auth success.
38
39use std::{
40    sync::{Arc, Mutex},
41    time::Duration,
42};
43
44pub type AuthResultSender = tokio::sync::oneshot::Sender<Result<(), String>>;
45pub type AuthResultReceiver = tokio::sync::oneshot::Receiver<Result<(), String>>;
46
47/// Generic authentication state tracker for WebSocket connections.
48///
49/// Coordinates authentication attempts by providing a channel-based signaling
50/// mechanism. Each authentication attempt receives a dedicated oneshot channel
51/// that will be resolved when the server responds.
52///
53/// # Superseding Behavior
54///
55/// If a new authentication attempt begins while a previous one is pending,
56/// the old attempt is automatically cancelled with an error. This prevents
57/// auth response race conditions during rapid reconnections.
58///
59/// # Thread Safety
60///
61/// All operations are thread-safe and can be called concurrently from multiple tasks.
62#[derive(Clone, Debug)]
63pub struct AuthTracker {
64    tx: Arc<Mutex<Option<AuthResultSender>>>,
65}
66
67impl AuthTracker {
68    /// Creates a new authentication tracker.
69    pub fn new() -> Self {
70        Self {
71            tx: Arc::new(Mutex::new(None)),
72        }
73    }
74
75    /// Begins a new authentication attempt.
76    ///
77    /// Returns a receiver that will be notified when authentication completes.
78    /// If a previous authentication attempt is still pending, it will be cancelled
79    /// with an error message indicating it was superseded.
80    pub fn begin(&self) -> AuthResultReceiver {
81        let (sender, receiver) = tokio::sync::oneshot::channel();
82
83        if let Ok(mut guard) = self.tx.lock() {
84            if let Some(old) = guard.take() {
85                tracing::warn!("New authentication request superseding previous pending request");
86                let _ = old.send(Err("Authentication attempt superseded".to_string()));
87            } else {
88                tracing::debug!("Starting new authentication request");
89            }
90            *guard = Some(sender);
91        }
92
93        receiver
94    }
95
96    /// Marks the current authentication attempt as successful.
97    ///
98    /// Notifies the waiting receiver with `Ok(())`. This should be called
99    /// when the server sends a successful authentication response.
100    ///
101    /// If no authentication attempt is pending, this is a no-op.
102    pub fn succeed(&self) {
103        if let Ok(mut guard) = self.tx.lock()
104            && let Some(sender) = guard.take()
105        {
106            let _ = sender.send(Ok(()));
107        }
108    }
109
110    /// Marks the current authentication attempt as failed.
111    ///
112    /// Notifies the waiting receiver with `Err(message)`. This should be called
113    /// when the server sends an authentication error response.
114    ///
115    /// If no authentication attempt is pending, this is a no-op.
116    pub fn fail(&self, error: impl Into<String>) {
117        let message = error.into();
118        if let Ok(mut guard) = self.tx.lock()
119            && let Some(sender) = guard.take()
120        {
121            let _ = sender.send(Err(message));
122        }
123    }
124
125    /// Waits for the authentication result with a timeout.
126    ///
127    /// Returns `Ok(())` if authentication succeeds, or an error if it fails,
128    /// times out, or the channel is closed.
129    ///
130    /// # Type Parameters
131    ///
132    /// - `E`: Error type that implements `From<String>` for error message conversion
133    ///
134    /// # Errors
135    ///
136    /// Returns an error in the following cases:
137    /// - Authentication fails (server rejects credentials)
138    /// - Authentication times out (no response within timeout duration)
139    /// - Authentication channel closes unexpectedly
140    /// - Authentication attempt is superseded by a new attempt
141    pub async fn wait_for_result<E>(
142        &self,
143        timeout: Duration,
144        receiver: AuthResultReceiver,
145    ) -> Result<(), E>
146    where
147        E: From<String>,
148    {
149        match tokio::time::timeout(timeout, receiver).await {
150            Ok(Ok(Ok(()))) => Ok(()),
151            Ok(Ok(Err(msg))) => Err(E::from(msg)),
152            Ok(Err(_)) => Err(E::from("Authentication channel closed".to_string())),
153            Err(_) => {
154                // Clear the sender on timeout to prevent memory leak
155                if let Ok(mut guard) = self.tx.lock() {
156                    guard.take();
157                }
158                Err(E::from("Authentication timed out".to_string()))
159            }
160        }
161    }
162}
163
164impl Default for AuthTracker {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use std::{
173        sync::atomic::{AtomicBool, Ordering},
174        time::Duration,
175    };
176
177    use rstest::rstest;
178
179    use super::*;
180
181    #[derive(Debug, PartialEq)]
182    struct TestError(String);
183
184    impl From<String> for TestError {
185        fn from(msg: String) -> Self {
186            Self(msg)
187        }
188    }
189
190    #[rstest]
191    #[tokio::test]
192    async fn test_successful_authentication() {
193        let tracker = AuthTracker::new();
194        let rx = tracker.begin();
195
196        tracker.succeed();
197
198        let result: Result<(), TestError> =
199            tracker.wait_for_result(Duration::from_secs(1), rx).await;
200
201        assert!(result.is_ok());
202    }
203
204    #[rstest]
205    #[tokio::test]
206    async fn test_failed_authentication() {
207        let tracker = AuthTracker::new();
208        let rx = tracker.begin();
209
210        tracker.fail("Invalid credentials");
211
212        let result: Result<(), TestError> =
213            tracker.wait_for_result(Duration::from_secs(1), rx).await;
214
215        assert_eq!(
216            result.unwrap_err(),
217            TestError("Invalid credentials".to_string())
218        );
219    }
220
221    #[rstest]
222    #[tokio::test]
223    async fn test_authentication_timeout() {
224        let tracker = AuthTracker::new();
225        let rx = tracker.begin();
226
227        // Don't call succeed or fail - let it timeout
228
229        let result: Result<(), TestError> =
230            tracker.wait_for_result(Duration::from_millis(50), rx).await;
231
232        assert_eq!(
233            result.unwrap_err(),
234            TestError("Authentication timed out".to_string())
235        );
236    }
237
238    #[rstest]
239    #[tokio::test]
240    async fn test_begin_supersedes_previous_sender() {
241        let tracker = AuthTracker::new();
242
243        let first = tracker.begin();
244        let second = tracker.begin();
245
246        // First receiver should get superseded error
247        let result = first.await.expect("oneshot closed unexpectedly");
248        assert_eq!(result, Err("Authentication attempt superseded".to_string()));
249
250        // Second attempt should succeed
251        tracker.succeed();
252        let result: Result<(), TestError> = tracker
253            .wait_for_result(Duration::from_secs(1), second)
254            .await;
255
256        assert!(result.is_ok());
257    }
258
259    #[rstest]
260    #[tokio::test]
261    async fn test_succeed_without_pending_auth() {
262        let tracker = AuthTracker::new();
263
264        // Calling succeed without begin should not panic
265        tracker.succeed();
266    }
267
268    #[rstest]
269    #[tokio::test]
270    async fn test_fail_without_pending_auth() {
271        let tracker = AuthTracker::new();
272
273        // Calling fail without begin should not panic
274        tracker.fail("Some error");
275    }
276
277    #[rstest]
278    #[tokio::test]
279    async fn test_multiple_sequential_authentications() {
280        let tracker = AuthTracker::new();
281
282        // First auth succeeds
283        let rx1 = tracker.begin();
284        tracker.succeed();
285        let result1: Result<(), TestError> =
286            tracker.wait_for_result(Duration::from_secs(1), rx1).await;
287        assert!(result1.is_ok());
288
289        // Second auth fails
290        let rx2 = tracker.begin();
291        tracker.fail("Credentials expired");
292        let result2: Result<(), TestError> =
293            tracker.wait_for_result(Duration::from_secs(1), rx2).await;
294        assert_eq!(
295            result2.unwrap_err(),
296            TestError("Credentials expired".to_string())
297        );
298
299        // Third auth succeeds
300        let rx3 = tracker.begin();
301        tracker.succeed();
302        let result3: Result<(), TestError> =
303            tracker.wait_for_result(Duration::from_secs(1), rx3).await;
304        assert!(result3.is_ok());
305    }
306
307    #[rstest]
308    #[tokio::test]
309    async fn test_channel_closed_before_result() {
310        let tracker = AuthTracker::new();
311        let rx = tracker.begin();
312
313        // Drop the tracker's sender by starting a new auth
314        tracker.begin();
315
316        // Original receiver should get channel closed error
317        let result: Result<(), TestError> =
318            tracker.wait_for_result(Duration::from_secs(1), rx).await;
319
320        assert_eq!(
321            result.unwrap_err(),
322            TestError("Authentication attempt superseded".to_string())
323        );
324    }
325
326    #[rstest]
327    #[tokio::test]
328    async fn test_concurrent_auth_attempts() {
329        let tracker = Arc::new(AuthTracker::new());
330        let mut handles = vec![];
331
332        // Spawn 10 concurrent auth attempts
333        for i in 0..10 {
334            let tracker_clone = Arc::clone(&tracker);
335            let handle = tokio::spawn(async move {
336                let rx = tracker_clone.begin();
337
338                // Only the last one should succeed
339                if i == 9 {
340                    tokio::time::sleep(Duration::from_millis(10)).await;
341                    tracker_clone.succeed();
342                }
343
344                let result: Result<(), TestError> = tracker_clone
345                    .wait_for_result(Duration::from_secs(1), rx)
346                    .await;
347
348                (i, result)
349            });
350            handles.push(handle);
351        }
352
353        let mut successes = 0;
354        let mut superseded = 0;
355
356        for handle in handles {
357            let (i, result) = handle.await.unwrap();
358            match result {
359                Ok(()) => {
360                    // Only task 9 should succeed
361                    assert_eq!(i, 9);
362                    successes += 1;
363                }
364                Err(TestError(msg)) if msg.contains("superseded") => {
365                    superseded += 1;
366                }
367                Err(e) => panic!("Unexpected error: {e:?}"),
368            }
369        }
370
371        assert_eq!(successes, 1);
372        assert_eq!(superseded, 9);
373    }
374
375    #[rstest]
376    fn test_default_trait() {
377        let _tracker = AuthTracker::default();
378    }
379
380    #[rstest]
381    #[tokio::test]
382    async fn test_clone_trait() {
383        let tracker = AuthTracker::new();
384        let cloned = tracker.clone();
385
386        // Verify cloned instance shares state with original (Arc behavior)
387        let rx = tracker.begin();
388        cloned.succeed(); // Succeed via clone affects original
389        let result: Result<(), TestError> =
390            tracker.wait_for_result(Duration::from_secs(1), rx).await;
391        assert!(result.is_ok());
392    }
393
394    #[rstest]
395    fn test_debug_trait() {
396        let tracker = AuthTracker::new();
397        let debug_str = format!("{tracker:?}");
398        assert!(debug_str.contains("AuthTracker"));
399    }
400
401    #[rstest]
402    #[tokio::test]
403    async fn test_timeout_clears_sender() {
404        let tracker = AuthTracker::new();
405
406        // Start auth that will timeout
407        let rx1 = tracker.begin();
408        let result1: Result<(), TestError> = tracker
409            .wait_for_result(Duration::from_millis(50), rx1)
410            .await;
411        assert_eq!(
412            result1.unwrap_err(),
413            TestError("Authentication timed out".to_string())
414        );
415
416        // Verify sender was cleared - new auth should work
417        let rx2 = tracker.begin();
418        tracker.succeed();
419        let result2: Result<(), TestError> =
420            tracker.wait_for_result(Duration::from_secs(1), rx2).await;
421        assert!(result2.is_ok());
422    }
423
424    #[rstest]
425    #[tokio::test]
426    async fn test_fail_clears_sender() {
427        let tracker = AuthTracker::new();
428
429        // Auth fails
430        let rx1 = tracker.begin();
431        tracker.fail("Bad credentials");
432        let result1: Result<(), TestError> =
433            tracker.wait_for_result(Duration::from_secs(1), rx1).await;
434        assert!(result1.is_err());
435
436        // Verify sender was cleared - new auth should work
437        let rx2 = tracker.begin();
438        tracker.succeed();
439        let result2: Result<(), TestError> =
440            tracker.wait_for_result(Duration::from_secs(1), rx2).await;
441        assert!(result2.is_ok());
442    }
443
444    #[rstest]
445    #[tokio::test]
446    async fn test_succeed_clears_sender() {
447        let tracker = AuthTracker::new();
448
449        // Auth succeeds
450        let rx1 = tracker.begin();
451        tracker.succeed();
452        let result1: Result<(), TestError> =
453            tracker.wait_for_result(Duration::from_secs(1), rx1).await;
454        assert!(result1.is_ok());
455
456        // Verify sender was cleared - new auth should work
457        let rx2 = tracker.begin();
458        tracker.succeed();
459        let result2: Result<(), TestError> =
460            tracker.wait_for_result(Duration::from_secs(1), rx2).await;
461        assert!(result2.is_ok());
462    }
463
464    #[rstest]
465    #[tokio::test]
466    async fn test_rapid_begin_succeed_cycles() {
467        let tracker = AuthTracker::new();
468
469        // Rapidly cycle through auth attempts
470        for _ in 0..100 {
471            let rx = tracker.begin();
472            tracker.succeed();
473            let result: Result<(), TestError> =
474                tracker.wait_for_result(Duration::from_secs(1), rx).await;
475            assert!(result.is_ok());
476        }
477    }
478
479    #[rstest]
480    #[tokio::test]
481    async fn test_double_succeed_is_safe() {
482        let tracker = AuthTracker::new();
483        let rx = tracker.begin();
484
485        // Call succeed twice
486        tracker.succeed();
487        tracker.succeed(); // Second call should be no-op
488
489        let result: Result<(), TestError> =
490            tracker.wait_for_result(Duration::from_secs(1), rx).await;
491        assert!(result.is_ok());
492    }
493
494    #[rstest]
495    #[tokio::test]
496    async fn test_double_fail_is_safe() {
497        let tracker = AuthTracker::new();
498        let rx = tracker.begin();
499
500        // Call fail twice
501        tracker.fail("Error 1");
502        tracker.fail("Error 2"); // Second call should be no-op
503
504        let result: Result<(), TestError> =
505            tracker.wait_for_result(Duration::from_secs(1), rx).await;
506        assert_eq!(
507            result.unwrap_err(),
508            TestError("Error 1".to_string()) // Should be first error
509        );
510    }
511
512    #[rstest]
513    #[tokio::test]
514    async fn test_succeed_after_fail_is_ignored() {
515        let tracker = AuthTracker::new();
516        let rx = tracker.begin();
517
518        tracker.fail("Auth failed");
519        tracker.succeed(); // This should be no-op
520
521        let result: Result<(), TestError> =
522            tracker.wait_for_result(Duration::from_secs(1), rx).await;
523        assert!(result.is_err()); // Should still be error
524    }
525
526    #[rstest]
527    #[tokio::test]
528    async fn test_fail_after_succeed_is_ignored() {
529        let tracker = AuthTracker::new();
530        let rx = tracker.begin();
531
532        tracker.succeed();
533        tracker.fail("Auth failed"); // This should be no-op
534
535        let result: Result<(), TestError> =
536            tracker.wait_for_result(Duration::from_secs(1), rx).await;
537        assert!(result.is_ok()); // Should still be success
538    }
539
540    /// Simulates a reconnect flow where authentication must complete before resubscription.
541    ///
542    /// This is an integration-style test that verifies:
543    /// 1. On reconnect, authentication starts first
544    /// 2. Subscription logic waits for auth to complete
545    /// 3. Subscriptions only proceed after successful auth
546    #[rstest]
547    #[tokio::test]
548    async fn test_reconnect_flow_waits_for_auth() {
549        let tracker = Arc::new(AuthTracker::new());
550        let subscribed = Arc::new(tokio::sync::Notify::new());
551        let auth_completed = Arc::new(tokio::sync::Notify::new());
552
553        // Simulate reconnect handler
554        let tracker_reconnect = Arc::clone(&tracker);
555        let subscribed_reconnect = Arc::clone(&subscribed);
556        let auth_completed_reconnect = Arc::clone(&auth_completed);
557
558        let reconnect_task = tokio::spawn(async move {
559            // Step 1: Begin authentication
560            let rx = tracker_reconnect.begin();
561
562            // Step 2: Spawn resubscription task that waits for auth
563            let tracker_resub = Arc::clone(&tracker_reconnect);
564            let subscribed_resub = Arc::clone(&subscribed_reconnect);
565            let auth_completed_resub = Arc::clone(&auth_completed_reconnect);
566
567            let resub_task = tokio::spawn(async move {
568                // Wait for auth to complete
569                let result: Result<(), TestError> = tracker_resub
570                    .wait_for_result(Duration::from_secs(5), rx)
571                    .await;
572
573                if result.is_ok() {
574                    auth_completed_resub.notify_one();
575                    // Simulate resubscription
576                    tokio::time::sleep(Duration::from_millis(10)).await;
577                    subscribed_resub.notify_one();
578                }
579            });
580
581            resub_task.await.unwrap();
582        });
583
584        // Simulate server auth response after delay
585        tokio::time::sleep(Duration::from_millis(100)).await;
586        tracker.succeed();
587
588        // Wait for reconnect flow to complete
589        reconnect_task.await.unwrap();
590
591        // Verify auth completed before subscription
592        tokio::select! {
593            _ = auth_completed.notified() => {
594                // Good - auth completed
595            }
596            _ = tokio::time::sleep(Duration::from_secs(1)) => {
597                panic!("Auth never completed");
598            }
599        }
600
601        // Verify subscription completed
602        tokio::select! {
603            _ = subscribed.notified() => {
604                // Good - subscribed
605            }
606            _ = tokio::time::sleep(Duration::from_secs(1)) => {
607                panic!("Subscription never completed");
608            }
609        }
610    }
611
612    /// Verifies that failed authentication prevents resubscription in reconnect flow.
613    #[rstest]
614    #[tokio::test]
615    async fn test_reconnect_flow_blocks_on_auth_failure() {
616        let tracker = Arc::new(AuthTracker::new());
617        let subscribed = Arc::new(AtomicBool::new(false));
618
619        let tracker_reconnect = Arc::clone(&tracker);
620        let subscribed_reconnect = Arc::clone(&subscribed);
621
622        let reconnect_task = tokio::spawn(async move {
623            let rx = tracker_reconnect.begin();
624
625            // Spawn resubscription task that waits for auth
626            let tracker_resub = Arc::clone(&tracker_reconnect);
627            let subscribed_resub = Arc::clone(&subscribed_reconnect);
628
629            let resub_task = tokio::spawn(async move {
630                let result: Result<(), TestError> = tracker_resub
631                    .wait_for_result(Duration::from_secs(5), rx)
632                    .await;
633
634                // Only subscribe if auth succeeds
635                if result.is_ok() {
636                    subscribed_resub.store(true, Ordering::Relaxed);
637                }
638            });
639
640            resub_task.await.unwrap();
641        });
642
643        // Simulate server auth failure
644        tokio::time::sleep(Duration::from_millis(50)).await;
645        tracker.fail("Invalid credentials");
646
647        // Wait for reconnect flow to complete
648        reconnect_task.await.unwrap();
649
650        // Verify subscription never happened
651        tokio::time::sleep(Duration::from_millis(100)).await;
652        assert!(!subscribed.load(Ordering::Relaxed));
653    }
654
655    /// Tests state machine transitions exhaustively.
656    #[rstest]
657    #[tokio::test]
658    async fn test_state_machine_transitions() {
659        let tracker = AuthTracker::new();
660
661        // Transition 1: Initial -> Pending (begin)
662        let rx1 = tracker.begin();
663
664        // Transition 2: Pending -> Success (succeed)
665        tracker.succeed();
666        let result1: Result<(), TestError> =
667            tracker.wait_for_result(Duration::from_secs(1), rx1).await;
668        assert!(result1.is_ok());
669
670        // Transition 3: Success -> Pending (begin again)
671        let rx2 = tracker.begin();
672
673        // Transition 4: Pending -> Failure (fail)
674        tracker.fail("Error");
675        let result2: Result<(), TestError> =
676            tracker.wait_for_result(Duration::from_secs(1), rx2).await;
677        assert!(result2.is_err());
678
679        // Transition 5: Failure -> Pending (begin again)
680        let rx3 = tracker.begin();
681
682        // Transition 6: Pending -> Timeout
683        let result3: Result<(), TestError> = tracker
684            .wait_for_result(Duration::from_millis(50), rx3)
685            .await;
686        assert_eq!(
687            result3.unwrap_err(),
688            TestError("Authentication timed out".to_string())
689        );
690
691        // Transition 7: Timeout -> Pending (begin again)
692        let rx4 = tracker.begin();
693
694        // Transition 8: Pending -> Superseded (begin interrupts)
695        let rx5 = tracker.begin();
696        let result4: Result<(), TestError> =
697            tracker.wait_for_result(Duration::from_secs(1), rx4).await;
698        assert_eq!(
699            result4.unwrap_err(),
700            TestError("Authentication attempt superseded".to_string())
701        );
702
703        // Final success to clean up
704        tracker.succeed();
705        let result5: Result<(), TestError> =
706            tracker.wait_for_result(Duration::from_secs(1), rx5).await;
707        assert!(result5.is_ok());
708    }
709
710    /// Verifies no memory leaks from orphaned senders.
711    #[rstest]
712    #[tokio::test]
713    async fn test_no_sender_leaks() {
714        let tracker = AuthTracker::new();
715
716        for _ in 0..100 {
717            let rx = tracker.begin();
718            let _result: Result<(), TestError> =
719                tracker.wait_for_result(Duration::from_millis(1), rx).await;
720        }
721
722        let rx = tracker.begin();
723        tracker.succeed();
724        let result: Result<(), TestError> =
725            tracker.wait_for_result(Duration::from_secs(1), rx).await;
726        assert!(result.is_ok());
727    }
728
729    /// Tests concurrent success/fail calls don't cause panics.
730    #[rstest]
731    #[tokio::test]
732    async fn test_concurrent_succeed_fail_calls() {
733        let tracker = Arc::new(AuthTracker::new());
734        let rx = tracker.begin();
735
736        let mut handles = vec![];
737
738        // Spawn many tasks trying to succeed
739        for _ in 0..50 {
740            let tracker_clone = Arc::clone(&tracker);
741            handles.push(tokio::spawn(async move {
742                tracker_clone.succeed();
743            }));
744        }
745
746        // Spawn many tasks trying to fail
747        for _ in 0..50 {
748            let tracker_clone = Arc::clone(&tracker);
749            handles.push(tokio::spawn(async move {
750                tracker_clone.fail("Error");
751            }));
752        }
753
754        // Wait for all tasks
755        for handle in handles {
756            handle.await.unwrap();
757        }
758
759        // Should get either success or failure, but not panic
760        let result: Result<(), TestError> =
761            tracker.wait_for_result(Duration::from_secs(1), rx).await;
762        // Don't care which outcome, just that it doesn't panic
763        let _ = result;
764    }
765}