1use std::pin::pin;
8
9use bitwarden_threading::time;
10use futures::future::{Either, select};
11use reqwest::StatusCode;
12use serde_json::json;
13use zeroize::Zeroizing;
14
15use super::{
16 super::{
17 error::OnePasswordError,
18 rest::RestClient,
19 wire::{self, CredentialBundle},
20 },
21 Timing,
22 cpace::perform_cpace,
23 ui::{
24 EnrollmentStatus, SsoEnrollmentContext, SsoEnrollmentResult, SsoUi, VerificationCodeResult,
25 },
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum ServerStatus {
31 SelectingVerifier,
33 WaitingForCode,
35 Denied,
37 Unknown,
39}
40
41impl ServerStatus {
42 fn parse(status: &str) -> ServerStatus {
43 match status {
44 "SELECTING_VERIFIER" => ServerStatus::SelectingVerifier,
45 "WAITING_FOR_CODE" => ServerStatus::WaitingForCode,
46 _ => ServerStatus::Unknown,
47 }
48 }
49}
50
51enum Stop {
53 Canceled,
54 Denied,
55 Failed(OnePasswordError),
56}
57
58impl From<OnePasswordError> for Stop {
59 fn from(error: OnePasswordError) -> Stop {
60 Stop::Failed(error)
61 }
62}
63
64impl From<Stop> for OnePasswordError {
65 fn from(stop: Stop) -> OnePasswordError {
66 match stop {
67 Stop::Canceled => OnePasswordError::Canceled,
68 Stop::Denied => OnePasswordError::Denied,
69 Stop::Failed(error) => error,
70 }
71 }
72}
73
74pub(super) async fn enroll_device(
81 username: &str,
82 sign_in_address: &str,
83 sign_in_token: &str,
84 ui: &dyn SsoUi,
85 rest: &RestClient,
86 timing: &Timing,
87) -> Result<CredentialBundle, OnePasswordError> {
88 let context = ui.begin_sso_enrollment().await;
89 let context = &*context;
90 let mut enrollment_uuid = None;
91
92 let outcome = {
94 let cancelled = pin!(context.cancelled());
95 let enrollment = pin!(enroll(
96 username,
97 sign_in_address,
98 sign_in_token,
99 context,
100 rest,
101 timing,
102 &mut enrollment_uuid,
103 ));
104 match select(cancelled, enrollment).await {
105 Either::Left(_) => Err(Stop::Canceled),
106 Either::Right((outcome, _)) => outcome,
107 }
108 };
109
110 let result = match &outcome {
111 Ok(_) => SsoEnrollmentResult::Success,
112 Err(Stop::Canceled) => SsoEnrollmentResult::Canceled,
113 Err(Stop::Denied) => SsoEnrollmentResult::Denied,
114 Err(Stop::Failed(_)) => SsoEnrollmentResult::Failed,
115 };
116
117 if matches!(outcome, Err(Stop::Canceled | Stop::Failed(_)))
119 && let Some(enrollment_uuid) = &enrollment_uuid
120 {
121 cancel_enrollment(sign_in_token, enrollment_uuid, rest).await;
122 }
123 context.end_enrollment(result).await;
124
125 outcome.map_err(OnePasswordError::from)
126}
127
128async fn enroll(
131 username: &str,
132 sign_in_address: &str,
133 sign_in_token: &str,
134 context: &dyn SsoEnrollmentContext,
135 rest: &RestClient,
136 timing: &Timing,
137 started: &mut Option<String>,
138) -> Result<CredentialBundle, Stop> {
139 context
141 .update_status(
142 EnrollmentStatus::WaitingForApproval,
143 "Waiting for approval from enrolled device...",
144 )
145 .await;
146 let enrollment_uuid = started.insert(start_enrollment(sign_in_token, rest).await?);
147
148 let status = poll_status_until_done(
150 &[ServerStatus::SelectingVerifier],
151 sign_in_token,
152 enrollment_uuid,
153 rest,
154 timing,
155 )
156 .await?;
157 if valid_status(status)? != ServerStatus::WaitingForCode {
158 return Err(unknown_status(status));
159 }
160
161 let verification_code = {
164 let user_input = pin!(context.provide_verification_code());
165 let poll = pin!(poll_status_until_done(
166 &[ServerStatus::WaitingForCode],
167 sign_in_token,
168 enrollment_uuid,
169 rest,
170 timing,
171 ));
172 match select(user_input, poll).await {
173 Either::Left((VerificationCodeResult::Code(code), _)) => Zeroizing::new(code),
174 Either::Left((VerificationCodeResult::Cancel, _)) => return Err(Stop::Canceled),
175 Either::Right((polled, _)) => return Err(unknown_status(valid_status(polled?)?)),
177 }
178 };
179
180 context
181 .update_status(
182 EnrollmentStatus::ExchangingCredentials,
183 "Exchanging credentials securely",
184 )
185 .await;
186 let credential_bundle = perform_cpace(
187 username,
188 sign_in_address,
189 sign_in_token,
190 enrollment_uuid,
191 &verification_code,
192 rest,
193 timing,
194 )
195 .await?;
196
197 context
198 .update_status(EnrollmentStatus::Completing, "Completing enrollment")
199 .await;
200 Ok(credential_bundle)
201}
202
203fn valid_status(status: ServerStatus) -> Result<ServerStatus, Stop> {
205 match status {
206 ServerStatus::Denied => Err(Stop::Denied),
207 status => Ok(status),
208 }
209}
210
211fn unknown_status(status: ServerStatus) -> Stop {
213 Stop::Failed(OnePasswordError::Internal(format!(
214 "device enrollment failed with unknown status {status:?}"
215 )))
216}
217
218async fn start_enrollment(
220 sign_in_token: &str,
221 rest: &RestClient,
222) -> Result<String, OnePasswordError> {
223 let info: wire::SsoEnrollInfo = rest
224 .post_json(
225 "v3/device/enrollments",
226 json!({ "signInToken": sign_in_token }),
227 )
228 .await?;
229
230 Ok(info.enrollment_uuid)
231}
232
233async fn cancel_enrollment(sign_in_token: &str, enrollment_uuid: &str, rest: &RestClient) {
236 let _ = rest
237 .delete_json(
238 "v2/device/enrollments",
239 json!({
240 "enrollmentUuid": enrollment_uuid,
241 "signInToken": sign_in_token,
242 }),
243 )
244 .await;
245}
246
247async fn poll_status_until_done(
249 ignore: &[ServerStatus],
250 sign_in_token: &str,
251 enrollment_uuid: &str,
252 rest: &RestClient,
253 timing: &Timing,
254) -> Result<ServerStatus, OnePasswordError> {
255 for _ in 0..timing.enrollment_poll_attempts() {
256 let status = get_enrollment_status(sign_in_token, enrollment_uuid, rest).await?;
257 if !ignore.contains(&status) {
258 return Ok(status);
259 }
260
261 time::sleep(timing.enrollment_poll_interval).await;
262 }
263
264 Err(OnePasswordError::Internal(
265 "device enrollment timed out".into(),
266 ))
267}
268
269async fn get_enrollment_status(
271 sign_in_token: &str,
272 enrollment_uuid: &str,
273 rest: &RestClient,
274) -> Result<ServerStatus, OnePasswordError> {
275 let response: Option<wire::SsoEnrollStatus> = rest
277 .post_json_unless_status(
278 "v3/device/enrollments/status",
279 json!({
280 "signInToken": sign_in_token,
281 "enrollmentUuid": enrollment_uuid,
282 }),
283 StatusCode::NOT_FOUND,
284 )
285 .await?;
286
287 Ok(response.map_or(ServerStatus::Denied, |response| {
288 ServerStatus::parse(&response.status)
289 }))
290}
291
292#[cfg(test)]
293mod tests {
294 use std::{
295 sync::{Arc, Mutex},
296 time::Duration,
297 };
298
299 use async_trait::async_trait;
300 use serde_json::json;
301 use tokio::sync::Notify;
302 use wiremock::{Mock, MockBuilder, MockServer, Request, ResponseTemplate, matchers};
303
304 use super::{
305 super::{
306 test_support::{
307 CpaceVectors, SIGN_IN_TOKEN, mock_msga, path, quick_timing, rest_client, vectors,
308 },
309 ui::SsoLoginResult,
310 },
311 *,
312 };
313
314 enum Prompt {
316 Code(String),
317 Cancel,
318 Pending,
320 }
321
322 #[derive(Debug, PartialEq)]
323 enum Event {
324 Status(EnrollmentStatus, String),
325 Ended(SsoEnrollmentResult),
326 }
327
328 fn status(status: EnrollmentStatus, message: &str) -> Event {
329 Event::Status(status, message.into())
330 }
331
332 #[derive(Clone, Default)]
334 struct Script {
335 events: Arc<Mutex<Vec<Event>>>,
336 cancel: Arc<Notify>,
337 }
338
339 impl Script {
340 fn cancel(&self) {
342 self.cancel.notify_one();
343 }
344
345 fn take_events(&self) -> Vec<Event> {
346 std::mem::take(&mut *self.events.lock().expect("not poisoned"))
347 }
348 }
349
350 struct ScriptedContext {
351 script: Script,
352 prompt: Prompt,
353 }
354
355 #[async_trait]
356 impl SsoEnrollmentContext for ScriptedContext {
357 async fn cancelled(&self) {
358 self.script.cancel.notified().await;
359 }
360
361 async fn update_status(&self, status: EnrollmentStatus, message: &str) {
362 self.script
363 .events
364 .lock()
365 .expect("not poisoned")
366 .push(Event::Status(status, message.into()));
367 }
368
369 async fn provide_verification_code(&self) -> VerificationCodeResult {
370 match &self.prompt {
371 Prompt::Code(code) => VerificationCodeResult::Code(code.clone()),
372 Prompt::Cancel => VerificationCodeResult::Cancel,
373 Prompt::Pending => std::future::pending().await,
374 }
375 }
376
377 async fn end_enrollment(&self, result: SsoEnrollmentResult) {
378 self.script
379 .events
380 .lock()
381 .expect("not poisoned")
382 .push(Event::Ended(result));
383 }
384 }
385
386 struct ScriptedUi(Mutex<Option<ScriptedContext>>);
388
389 #[async_trait]
390 impl SsoUi for ScriptedUi {
391 async fn perform_sso_login(&self, _: &str, _: &str) -> SsoLoginResult {
392 unreachable!("the enrollment does not log in")
393 }
394
395 async fn begin_sso_enrollment(&self) -> Box<dyn SsoEnrollmentContext> {
396 let context = self
397 .0
398 .lock()
399 .expect("not poisoned")
400 .take()
401 .expect("one enrollment per UI");
402 Box::new(context)
403 }
404 }
405
406 fn scripted(prompt: Prompt) -> (ScriptedUi, Script) {
407 let script = Script::default();
408 let ui = ScriptedUi(Mutex::new(Some(ScriptedContext {
409 script: script.clone(),
410 prompt,
411 })));
412 (ui, script)
413 }
414
415 fn code(code: &str) -> Prompt {
416 Prompt::Code(code.into())
417 }
418
419 async fn mock_start(server: &MockServer, vectors: &CpaceVectors) {
420 server
421 .register(
422 Mock::given(matchers::method("POST"))
423 .and(matchers::path("/api/v3/device/enrollments"))
424 .and(matchers::body_json(json!({"signInToken": SIGN_IN_TOKEN})))
425 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
426 "enrollmentUuid": vectors.enrollment_uuid,
427 "accountKeyUuid": "ACCOUNT_KEY_UUID",
428 })))
429 .expect(1),
430 )
431 .await;
432 }
433
434 fn status_mock(vectors: &CpaceVectors) -> MockBuilder {
435 Mock::given(matchers::method("POST"))
436 .and(matchers::path("/api/v3/device/enrollments/status"))
437 .and(matchers::body_json(json!({
438 "signInToken": SIGN_IN_TOKEN,
439 "enrollmentUuid": vectors.enrollment_uuid,
440 })))
441 }
442
443 fn reports(status: &str) -> ResponseTemplate {
444 ResponseTemplate::new(200).set_body_json(json!({ "status": status }))
445 }
446
447 fn denial() -> ResponseTemplate {
449 ResponseTemplate::new(404).set_body_json(json!({}))
450 }
451
452 async fn mock_statuses(
454 server: &MockServer,
455 vectors: &CpaceVectors,
456 mut answers: Vec<ResponseTemplate>,
457 ) {
458 let last = answers.pop().expect("at least one answer");
459 for answer in answers {
460 server
461 .register(status_mock(vectors).respond_with(answer).up_to_n_times(1))
462 .await;
463 }
464 server
465 .register(status_mock(vectors).respond_with(last))
466 .await;
467 }
468
469 async fn mock_cancel(server: &MockServer, vectors: &CpaceVectors, calls: u64) {
471 mock_cancel_answering(server, vectors, calls, ResponseTemplate::new(200)).await;
472 }
473
474 async fn mock_cancel_answering(
475 server: &MockServer,
476 vectors: &CpaceVectors,
477 calls: u64,
478 answer: ResponseTemplate,
479 ) {
480 server
481 .register(
482 Mock::given(matchers::method("DELETE"))
483 .and(matchers::path("/api/v2/device/enrollments"))
484 .and(matchers::body_json(json!({
485 "enrollmentUuid": vectors.enrollment_uuid,
486 "signInToken": SIGN_IN_TOKEN,
487 })))
488 .respond_with(answer)
489 .expect(calls),
490 )
491 .await;
492 }
493
494 async fn run_enrollment(
495 server: &MockServer,
496 vectors: &CpaceVectors,
497 ui: &ScriptedUi,
498 timing: &Timing,
499 ) -> Result<CredentialBundle, OnePasswordError> {
500 enroll_device(
501 &vectors.username,
502 &vectors.sign_in_address,
503 SIGN_IN_TOKEN,
504 ui,
505 &rest_client(server),
506 timing,
507 )
508 .await
509 }
510
511 fn assert_denied(result: Result<CredentialBundle, OnePasswordError>) {
512 assert!(matches!(result.err(), Some(OnePasswordError::Denied)));
513 }
514
515 #[tokio::test]
516 async fn a_failed_exchange_fails_the_enrollment() {
517 let vectors = vectors().cpace;
518 let server = MockServer::start().await;
519 mock_start(&server, &vectors).await;
520 mock_statuses(&server, &vectors, vec![reports("WAITING_FOR_CODE")]).await;
521 mock_msga(&server, &vectors).await;
522 server
523 .register(
524 Mock::given(matchers::method("PUT"))
525 .and(matchers::path(path(&vectors, "cpace/msgb")))
526 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": 0})))
527 .expect(1),
528 )
529 .await;
530 mock_cancel(&server, &vectors, 1).await;
531 let (ui, script) = scripted(code(&vectors.verification_code));
532
533 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
534 .await
535 .err()
536 .expect("the enrollment fails");
537
538 assert!(matches!(
539 &error,
540 OnePasswordError::Internal(message) if message.contains("exchange credentials")
541 ));
542 assert_eq!(
543 script.take_events(),
544 [
545 status(
546 EnrollmentStatus::WaitingForApproval,
547 "Waiting for approval from enrolled device..."
548 ),
549 status(
550 EnrollmentStatus::ExchangingCredentials,
551 "Exchanging credentials securely"
552 ),
553 Event::Ended(SsoEnrollmentResult::Failed),
554 ]
555 );
556 server.verify().await;
557 }
558
559 #[tokio::test]
560 async fn denying_the_request_cancels_the_login() {
561 let vectors = vectors().cpace;
562 let server = MockServer::start().await;
563 mock_start(&server, &vectors).await;
564 mock_statuses(
565 &server,
566 &vectors,
567 vec![reports("SELECTING_VERIFIER"), denial()],
568 )
569 .await;
570 mock_cancel(&server, &vectors, 0).await;
571 let (ui, script) = scripted(Prompt::Pending);
572
573 let result = run_enrollment(&server, &vectors, &ui, &quick_timing()).await;
574
575 assert_denied(result);
576 assert_eq!(
577 script.take_events(),
578 [
579 status(
580 EnrollmentStatus::WaitingForApproval,
581 "Waiting for approval from enrolled device..."
582 ),
583 Event::Ended(SsoEnrollmentResult::Denied),
584 ]
585 );
586 server.verify().await;
587 }
588
589 #[tokio::test]
590 async fn only_the_status_decides_a_denial() {
591 let vectors = vectors().cpace;
592 for (response, denied) in [
593 (
594 ResponseTemplate::new(404)
595 .set_body_json(json!({"errorCode": 102, "errorMessage": "other"})),
596 true,
597 ),
598 (
599 ResponseTemplate::new(400)
600 .set_body_json(json!({"errorCode": 117, "errorMessage": "not found"})),
601 false,
602 ),
603 ] {
604 let server = MockServer::start().await;
605 server
606 .register(status_mock(&vectors).respond_with(response))
607 .await;
608
609 let status = get_enrollment_status(
610 SIGN_IN_TOKEN,
611 &vectors.enrollment_uuid,
612 &rest_client(&server),
613 )
614 .await;
615
616 assert_eq!(status.ok() == Some(ServerStatus::Denied), denied);
617 }
618 }
619
620 #[tokio::test]
621 async fn a_not_found_error_body_is_a_denial_too() {
622 let vectors = vectors().cpace;
623 let server = MockServer::start().await;
624 mock_start(&server, &vectors).await;
625 mock_statuses(
626 &server,
627 &vectors,
628 vec![
629 ResponseTemplate::new(404)
630 .set_body_json(json!({"errorCode": 117, "errorMessage": "not found"})),
631 ],
632 )
633 .await;
634 let (ui, script) = scripted(Prompt::Pending);
635
636 let result = run_enrollment(&server, &vectors, &ui, &quick_timing()).await;
637
638 assert_denied(result);
639 assert_eq!(
640 script.take_events().last(),
641 Some(&Event::Ended(SsoEnrollmentResult::Denied))
642 );
643 server.verify().await;
644 }
645
646 #[tokio::test]
647 async fn denying_the_request_while_the_code_is_typed_is_a_denial() {
648 let vectors = vectors().cpace;
649 let server = MockServer::start().await;
650 mock_start(&server, &vectors).await;
651 mock_statuses(
652 &server,
653 &vectors,
654 vec![reports("WAITING_FOR_CODE"), denial()],
655 )
656 .await;
657 mock_cancel(&server, &vectors, 0).await;
658 let (ui, script) = scripted(Prompt::Pending);
659
660 let result = run_enrollment(&server, &vectors, &ui, &quick_timing()).await;
661
662 assert_denied(result);
663 assert_eq!(
664 script.take_events().last(),
665 Some(&Event::Ended(SsoEnrollmentResult::Denied))
666 );
667 server.verify().await;
668 }
669
670 #[tokio::test]
671 async fn polling_times_out() {
672 let vectors = vectors().cpace;
673 let server = MockServer::start().await;
674 mock_start(&server, &vectors).await;
675 server
676 .register(
677 status_mock(&vectors)
678 .respond_with(reports("SELECTING_VERIFIER"))
679 .expect(5),
680 )
681 .await;
682 mock_cancel(&server, &vectors, 1).await;
683 let (ui, script) = scripted(Prompt::Pending);
684 let timing = Timing {
685 enrollment_poll_timeout: Duration::from_millis(5),
686 ..quick_timing()
687 };
688
689 let error = run_enrollment(&server, &vectors, &ui, &timing)
690 .await
691 .err()
692 .expect("the enrollment fails");
693
694 assert!(matches!(
695 &error,
696 OnePasswordError::Internal(message) if message.contains("timed out")
697 ));
698 assert_eq!(
699 script.take_events().last(),
700 Some(&Event::Ended(SsoEnrollmentResult::Failed))
701 );
702 server.verify().await;
703 }
704
705 #[tokio::test]
706 async fn canceling_while_polling_cancels_the_enrollment_on_the_server() {
707 let vectors = vectors().cpace;
708 let server = MockServer::start().await;
709 mock_start(&server, &vectors).await;
710 let (ui, script) = scripted(Prompt::Pending);
711 let canceler = script.clone();
712 server
713 .register(status_mock(&vectors).respond_with(move |_: &Request| {
714 canceler.cancel();
715 reports("SELECTING_VERIFIER")
716 }))
717 .await;
718 mock_cancel(&server, &vectors, 1).await;
719
720 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
721 .await
722 .err()
723 .expect("the enrollment is canceled");
724
725 assert!(matches!(error, OnePasswordError::Canceled));
726 assert_eq!(
727 script.take_events(),
728 [
729 status(
730 EnrollmentStatus::WaitingForApproval,
731 "Waiting for approval from enrolled device..."
732 ),
733 Event::Ended(SsoEnrollmentResult::Canceled),
734 ]
735 );
736 server.verify().await;
737 }
738
739 #[tokio::test]
740 async fn canceling_before_the_server_knows_the_enrollment_sends_nothing() {
741 let vectors = vectors().cpace;
742 let server = MockServer::start().await;
743 let (ui, script) = scripted(Prompt::Pending);
744 script.cancel();
745
746 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
747 .await
748 .err()
749 .expect("the enrollment is canceled");
750
751 assert!(matches!(error, OnePasswordError::Canceled));
752 assert_eq!(
753 script.take_events(),
754 [Event::Ended(SsoEnrollmentResult::Canceled)]
755 );
756 assert!(
757 server
758 .received_requests()
759 .await
760 .expect("requests are recorded")
761 .is_empty()
762 );
763 }
764
765 #[tokio::test]
766 async fn canceling_at_the_code_prompt_cancels_the_enrollment_on_the_server() {
767 let vectors = vectors().cpace;
768 let server = MockServer::start().await;
769 mock_start(&server, &vectors).await;
770 mock_statuses(&server, &vectors, vec![reports("WAITING_FOR_CODE")]).await;
771 mock_cancel(&server, &vectors, 1).await;
772 let (ui, script) = scripted(Prompt::Cancel);
773
774 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
775 .await
776 .err()
777 .expect("the enrollment is canceled");
778
779 assert!(matches!(error, OnePasswordError::Canceled));
780 assert_eq!(
781 script.take_events(),
782 [
783 status(
784 EnrollmentStatus::WaitingForApproval,
785 "Waiting for approval from enrolled device..."
786 ),
787 Event::Ended(SsoEnrollmentResult::Canceled),
788 ]
789 );
790 server.verify().await;
791 }
792
793 #[tokio::test]
794 async fn a_failing_server_cancellation_is_ignored() {
795 let vectors = vectors().cpace;
796 let server = MockServer::start().await;
797 mock_start(&server, &vectors).await;
798 mock_statuses(&server, &vectors, vec![reports("WAITING_FOR_CODE")]).await;
799 mock_cancel_answering(&server, &vectors, 1, ResponseTemplate::new(500)).await;
800 let (ui, script) = scripted(Prompt::Cancel);
801
802 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
803 .await
804 .err()
805 .expect("the enrollment is canceled");
806
807 assert!(matches!(error, OnePasswordError::Canceled));
808 assert_eq!(
809 script.take_events().last(),
810 Some(&Event::Ended(SsoEnrollmentResult::Canceled))
811 );
812 server.verify().await;
813 }
814
815 #[tokio::test]
816 async fn the_poll_ending_before_the_code_arrives_fails_the_enrollment() {
817 let vectors = vectors().cpace;
818 let server = MockServer::start().await;
819 mock_start(&server, &vectors).await;
820 mock_statuses(
821 &server,
822 &vectors,
823 vec![reports("WAITING_FOR_CODE"), reports("SOMETHING_NEW")],
824 )
825 .await;
826 mock_cancel(&server, &vectors, 1).await;
827 let (ui, script) = scripted(Prompt::Pending);
828
829 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
830 .await
831 .err()
832 .expect("the enrollment fails");
833
834 assert!(matches!(
835 &error,
836 OnePasswordError::Internal(message) if message.contains("unknown status Unknown")
837 ));
838 assert_eq!(
839 script.take_events().last(),
840 Some(&Event::Ended(SsoEnrollmentResult::Failed))
841 );
842 server.verify().await;
843 }
844
845 #[tokio::test]
846 async fn an_unexpected_status_after_the_approval_wait_fails_the_enrollment() {
847 let vectors = vectors().cpace;
848 let server = MockServer::start().await;
849 mock_start(&server, &vectors).await;
850 mock_statuses(&server, &vectors, vec![reports("SOMETHING_NEW")]).await;
851 let (ui, script) = scripted(Prompt::Pending);
852
853 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
854 .await
855 .err()
856 .expect("the enrollment fails");
857
858 assert!(matches!(
859 &error,
860 OnePasswordError::Internal(message) if message.contains("unknown status Unknown")
861 ));
862 assert_eq!(
863 script.take_events().last(),
864 Some(&Event::Ended(SsoEnrollmentResult::Failed))
865 );
866 server.verify().await;
867 }
868
869 #[tokio::test]
870 async fn a_failing_start_fails_the_enrollment() {
871 let vectors = vectors().cpace;
872 let server = MockServer::start().await;
873 server
874 .register(
875 Mock::given(matchers::path("/api/v3/device/enrollments"))
876 .respond_with(ResponseTemplate::new(503))
877 .expect(1),
878 )
879 .await;
880 mock_cancel(&server, &vectors, 0).await;
881 let (ui, script) = scripted(Prompt::Pending);
882
883 let error = run_enrollment(&server, &vectors, &ui, &quick_timing())
884 .await
885 .err()
886 .expect("the enrollment fails");
887
888 assert!(matches!(
889 error,
890 OnePasswordError::UnexpectedStatus { status: 503, .. }
891 ));
892 assert_eq!(
893 script.take_events(),
894 [
895 status(
896 EnrollmentStatus::WaitingForApproval,
897 "Waiting for approval from enrolled device..."
898 ),
899 Event::Ended(SsoEnrollmentResult::Failed),
900 ]
901 );
902 server.verify().await;
903 }
904
905 #[test]
906 fn parses_server_statuses() {
907 assert_eq!(
908 ServerStatus::parse("SELECTING_VERIFIER"),
909 ServerStatus::SelectingVerifier
910 );
911 assert_eq!(
912 ServerStatus::parse("WAITING_FOR_CODE"),
913 ServerStatus::WaitingForCode
914 );
915 assert_eq!(ServerStatus::parse("DONE"), ServerStatus::Unknown);
916 assert_eq!(ServerStatus::parse(""), ServerStatus::Unknown);
917 }
918
919 #[test]
920 fn a_zero_poll_interval_never_polls() {
921 let timing = Timing {
922 enrollment_poll_interval: Duration::ZERO,
923 ..quick_timing()
924 };
925
926 assert_eq!(timing.enrollment_poll_attempts(), 0);
927 assert_eq!(Timing::default().enrollment_poll_attempts(), 150);
928 }
929
930 #[test]
931 fn the_enrollment_can_run_on_another_thread() {
932 fn assert_send(_: &impl Send) {}
933
934 let vectors = vectors().cpace;
935 let rest = RestClient::new(
936 reqwest::Client::new(),
937 "http://localhost",
938 "client",
939 "user agent",
940 "op user agent",
941 )
942 .expect("valid headers");
943 let (ui, _) = scripted(Prompt::Pending);
944 let timing = quick_timing();
945
946 let enrollment = enroll_device(
947 &vectors.username,
948 &vectors.sign_in_address,
949 SIGN_IN_TOKEN,
950 &ui,
951 &rest,
952 &timing,
953 );
954
955 assert_send(&enrollment);
956 }
957}