Skip to main content

bitwarden_importers/importers/onepassword/access/sso/
enrollment.rs

1//! Enrolling this device as a trusted one: ask the server to start an enrollment, wait for the user
2//! to approve it on an enrolled device, then take the credentials that device shares.
3//!
4//! The UI drives the enrollment through an `SsoEnrollmentContext`, which shows the progress, asks
5//! for the verification code and can cancel at any point.
6
7use 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/// What the server reports about an enrollment.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum ServerStatus {
31    /// The user has not picked an enrolled device to approve the request yet.
32    SelectingVerifier,
33    /// An enrolled device approved the request and shows the verification code.
34    WaitingForCode,
35    /// The user denied the request on the enrolled device.
36    Denied,
37    /// Anything else the server may report.
38    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
51/// Why an enrollment ended early. It also decides what `end_enrollment` reports.
52enum 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
74/// Enrolls this device and returns the credential bundle an enrolled device shared.
75///
76/// The context is closed with `end_enrollment` when this ends, unless the future is dropped first.
77/// An enrollment that is canceled, from the UI or at the verification code prompt, or that fails
78/// after the server handed out its uuid is also canceled on the server, so the approving device
79/// stops waiting.
80pub(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    // Checking the cancellation first makes it win a tie.
93    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    // A denied enrollment is already over on the server.
118    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
128/// The steps of the enrollment. The uuid goes to `started` as soon as the server hands it out, for
129/// the caller to cancel the enrollment should this be dropped halfway.
130async 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    // Starting signals the enrolled devices to prepare for handing the credentials over.
140    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    // The user has to press "Transfer key" on one of the enrolled devices.
149    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    // The enrolled device shows the verification code. Polling on makes denying it there
162    // noticed while the user types.
163    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            // Nothing but the user is supposed to end the wait.
176            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
203/// Denial ends the enrollment, any other status is up to the caller.
204fn valid_status(status: ServerStatus) -> Result<ServerStatus, Stop> {
205    match status {
206        ServerStatus::Denied => Err(Stop::Denied),
207        status => Ok(status),
208    }
209}
210
211/// A status that nothing expects at this point.
212fn unknown_status(status: ServerStatus) -> Stop {
213    Stop::Failed(OnePasswordError::Internal(format!(
214        "device enrollment failed with unknown status {status:?}"
215    )))
216}
217
218/// Starts an enrollment and returns its uuid.
219async 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
233/// Tells the server to drop the enrollment, so the enrolled devices stop waiting on it. Errors
234/// are ignored: there is nothing left to do about them.
235async 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
247/// Polls the status until it is one not in `ignore`.
248async 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
269/// Reads the status of the enrollment.
270async fn get_enrollment_status(
271    sign_in_token: &str,
272    enrollment_uuid: &str,
273    rest: &RestClient,
274) -> Result<ServerStatus, OnePasswordError> {
275    // Denying the request makes the server answer 404, whatever the body.
276    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    /// What the user does at the verification code prompt.
315    enum Prompt {
316        Code(String),
317        Cancel,
318        /// Never answers, like a user who is still typing.
319        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    /// The handles the test keeps on a scripted enrollment.
333    #[derive(Clone, Default)]
334    struct Script {
335        events: Arc<Mutex<Vec<Event>>>,
336        cancel: Arc<Notify>,
337    }
338
339    impl Script {
340        /// Makes `cancelled()` resolve, now or once it is asked for.
341        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    /// A UI that hands out one scripted enrollment.
387    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    /// The body the server sends when the user denies the request on the enrolled device.
448    fn denial() -> ResponseTemplate {
449        ResponseTemplate::new(404).set_body_json(json!({}))
450    }
451
452    /// Answers the status requests in turn, then keeps repeating the last answer.
453    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    /// Expects `calls` requests to cancel the enrollment on the server.
470    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}