1use std::sync::Arc;
2
3use bitwarden_api_api::models::{
4 CreateOrganizationInviteLinkRequestModel, RefreshOrganizationInviteLinkRequestModel,
5 UpdateInviteSupportConfirmRequestModel, UpdateOrganizationInviteLinkRequestModel,
6};
7use bitwarden_core::{
8 ApiError, FromClient, OrganizationId,
9 client::ApiConfigurations,
10 key_management::{KeySlotIds, SymmetricKeySlotId},
11 require,
12};
13use bitwarden_crypto::{EncString, KeyStore};
14use bitwarden_organization_crypto::invite::{Invite, InviteSecret};
15use http::StatusCode;
16
17use crate::{InviteLinkError, OrganizationInviteLink, OrganizationInviteLinkView};
18
19#[bitwarden_ffi::wasm_object]
22#[derive(FromClient)]
23pub struct InviteLinkAdminClient {
24 pub(crate) key_store: KeyStore<KeySlotIds>,
25 pub(crate) api_configurations: Arc<ApiConfigurations>,
26}
27
28#[allow(deprecated)]
32#[bitwarden_ffi::wasm_export]
33impl InviteLinkAdminClient {
34 pub async fn get(
36 &self,
37 organization_id: OrganizationId,
38 ) -> Result<Option<OrganizationInviteLinkView>, InviteLinkError> {
39 let response = match self
40 .api_configurations
41 .api_client
42 .organization_invite_links_api()
43 .get(organization_id.into())
44 .await
45 {
46 Ok(response) => response,
47 Err(ApiError::Response(rc)) if rc.status == StatusCode::NOT_FOUND => return Ok(None),
48 Err(e) => return Err(e.into()),
49 };
50
51 let mut ctx = self.key_store.context();
52 OrganizationInviteLink::try_from(response)
53 .and_then(|link| link.to_view(&mut ctx))
54 .map(Some)
55 }
56
57 pub async fn delete(&self, organization_id: OrganizationId) -> Result<(), InviteLinkError> {
59 self.api_configurations
60 .api_client
61 .organization_invite_links_api()
62 .delete(organization_id.into())
63 .await?;
64 Ok(())
65 }
66
67 pub async fn create(
69 &self,
70 organization_id: OrganizationId,
71 allowed_domains: Vec<String>,
72 supports_confirmation: bool,
73 ) -> Result<OrganizationInviteLinkView, InviteLinkError> {
74 if allowed_domains.is_empty() {
75 return Err(InviteLinkError::NoAllowedDomains);
76 }
77
78 let link = self
79 .create_invite_link(organization_id, allowed_domains, supports_confirmation)
80 .await?;
81
82 let mut ctx = self.key_store.context();
83 link.to_view(&mut ctx)
84 }
85
86 pub async fn refresh(
88 &self,
89 organization_id: OrganizationId,
90 supports_confirmation: bool,
91 ) -> Result<OrganizationInviteLinkView, InviteLinkError> {
92 let link = self
93 .refresh_invite_link(organization_id, supports_confirmation)
94 .await?;
95
96 let mut ctx = self.key_store.context();
97 link.to_view(&mut ctx)
98 }
99
100 pub async fn update_allowed_domains(
102 &self,
103 organization_id: OrganizationId,
104 allowed_domains: Vec<String>,
105 ) -> Result<OrganizationInviteLinkView, InviteLinkError> {
106 if allowed_domains.is_empty() {
107 return Err(InviteLinkError::NoAllowedDomains);
108 }
109
110 let response = self
111 .api_configurations
112 .api_client
113 .organization_invite_links_api()
114 .update(
115 organization_id.into(),
116 Some(UpdateOrganizationInviteLinkRequestModel { allowed_domains }),
117 )
118 .await?;
119
120 let mut ctx = self.key_store.context();
121 OrganizationInviteLink::try_from(response)?.to_view(&mut ctx)
122 }
123
124 pub async fn update_confirmation(
131 &self,
132 organization_id: OrganizationId,
133 supports_confirmation: bool,
134 ) -> Result<OrganizationInviteLinkView, InviteLinkError> {
135 let existing_link = self
139 .api_configurations
140 .api_client
141 .organization_invite_links_api()
142 .get(organization_id.into())
143 .await?;
144 let invite: Invite = require!(existing_link.invite).parse()?;
145
146 let result = self
147 .set_invite_confirmation(organization_id, invite, supports_confirmation)
148 .await?;
149 let mut ctx = self.key_store.context();
150 result.to_view(&mut ctx)
151 }
152
153 #[deprecated(
164 note = "Use `update_confirmation`, which returns an `OrganizationInviteLinkView`, instead"
165 )]
166 pub async fn set_invite_confirmation(
167 &self,
168 organization_id: OrganizationId,
169 invite: Invite,
170 supports_confirmation: bool,
171 ) -> Result<OrganizationInviteLink, InviteLinkError> {
172 let mut invite = invite;
173
174 {
177 let mut ctx = self.key_store.context();
178 let org_key = SymmetricKeySlotId::Organization(organization_id);
179 if supports_confirmation {
180 invite.enable_confirmation(org_key, &mut ctx)?;
181 } else {
182 invite.disable_confirmation();
183 }
184 }
185
186 let response = self
187 .api_configurations
188 .api_client
189 .organization_invite_links_api()
190 .update_invite_support_confirm(
191 organization_id.into(),
192 Some(UpdateInviteSupportConfirmRequestModel {
193 invite: String::from(&invite),
194 supports_confirmation: invite.supports_confirmation(),
195 }),
196 )
197 .await?;
198
199 OrganizationInviteLink::try_from(response)
200 }
201
202 #[deprecated(note = "Use `create`, which returns an `OrganizationInviteLinkView`, instead")]
210 pub async fn create_invite_link(
211 &self,
212 organization_id: OrganizationId,
213 allowed_domains: Vec<String>,
214 supports_confirmation: bool,
215 ) -> Result<OrganizationInviteLink, InviteLinkError> {
216 let invite = self
217 .make_invite(organization_id, supports_confirmation)
218 .await?;
219
220 let response = self
221 .api_configurations
222 .api_client
223 .organization_invite_links_api()
224 .create(
225 organization_id.into(),
226 Some(CreateOrganizationInviteLinkRequestModel {
227 allowed_domains,
228 invite: String::from(&invite),
229 supports_confirmation: invite.supports_confirmation(),
230 }),
231 )
232 .await?;
233
234 OrganizationInviteLink::try_from(response)
235 }
236
237 #[deprecated(note = "Use `refresh`, which returns an `OrganizationInviteLinkView`, instead")]
240 pub async fn refresh_invite_link(
241 &self,
242 organization_id: OrganizationId,
243 supports_confirmation: bool,
244 ) -> Result<OrganizationInviteLink, InviteLinkError> {
245 let invite = self
246 .make_invite(organization_id, supports_confirmation)
247 .await?;
248
249 let response = self
250 .api_configurations
251 .api_client
252 .organization_invite_links_api()
253 .refresh(
254 organization_id.into(),
255 Some(RefreshOrganizationInviteLinkRequestModel {
256 invite: String::from(&invite),
257 supports_confirmation: invite.supports_confirmation(),
258 }),
259 )
260 .await?;
261
262 OrganizationInviteLink::try_from(response)
263 }
264
265 #[deprecated(
268 note = "Use `create` or `refresh`, which returns an `OrganizationInviteLinkView`, instead"
269 )]
270 #[cfg_attr(feature = "wasm", wasm_bindgen(unchecked_return_type = "InviteSecret"))]
271 pub fn get_invite_secret(
272 &self,
273 organization_id: OrganizationId,
274 invite: Invite,
275 ) -> Result<InviteSecret, InviteLinkError> {
276 let mut ctx = self.key_store.context();
277 let org_key = SymmetricKeySlotId::Organization(organization_id);
278 let invite_key = invite.unseal_invite_key_with_organization_key(org_key, &mut ctx)?;
279 let invite_secret = invite.get_invite_secret(invite_key, &mut ctx)?;
280 Ok(invite_secret)
281 }
282
283 async fn make_invite(
285 &self,
286 organization_id: OrganizationId,
287 supports_confirmation: bool,
288 ) -> Result<Invite, InviteLinkError> {
289 let wrapped_private_key_response = self
290 .api_configurations
291 .api_client
292 .organizations_api()
293 .get_private_key(organization_id.into())
294 .await?;
295
296 let wrapped_private_key: EncString =
297 require!(wrapped_private_key_response.private_key).parse()?;
298
299 let mut ctx = self.key_store.context();
300 let org_key = SymmetricKeySlotId::Organization(organization_id);
301 let (_, mut invite) =
302 Invite::make_for_private_key(org_key, &wrapped_private_key, &mut ctx)?;
303
304 if !supports_confirmation {
306 invite.disable_confirmation();
307 }
308
309 Ok(invite)
310 }
311}
312
313#[cfg(test)]
314#[allow(deprecated)]
315mod tests {
316 use bitwarden_api_api::{
317 apis::{ApiClient, ResponseContent},
318 models::OrganizationInviteLinkResponseModel,
319 };
320 use bitwarden_core::{
321 Client, client::ApiConfigurations, key_management::create_test_crypto_with_user_and_org_key,
322 };
323 use bitwarden_crypto::{
324 PublicKeyEncryptionAlgorithm, SymmetricCryptoKey, SymmetricKeyAlgorithm,
325 };
326 use bitwarden_encoding::B64;
327
328 use super::*;
329 use crate::InviteLinkClientExt as _;
330
331 fn make_client(org_id: OrganizationId, api_client: ApiClient) -> InviteLinkAdminClient {
332 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
333 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
334 let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key);
335 InviteLinkAdminClient {
336 key_store,
337 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
338 }
339 }
340
341 fn wrapped_org_private_key(client: &InviteLinkAdminClient, org_id: OrganizationId) -> String {
344 let mut ctx = client.key_store.context();
345 let org_key = SymmetricKeySlotId::Organization(org_id);
346 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
347 ctx.wrap_private_key(org_key, private_key)
348 .unwrap()
349 .to_string()
350 }
351
352 fn echo_link_response(
355 org_id: uuid::Uuid,
356 allowed_domains: Vec<String>,
357 invite: String,
358 supports_confirmation: bool,
359 ) -> OrganizationInviteLinkResponseModel {
360 OrganizationInviteLinkResponseModel {
361 object: None,
362 id: Some(uuid::Uuid::new_v4()),
363 code: Some(uuid::Uuid::new_v4()),
364 organization_id: Some(org_id),
365 allowed_domains: Some(allowed_domains),
366 invite: Some(invite),
367 supports_confirmation: Some(supports_confirmation),
368 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
369 }
370 }
371
372 fn build_invite(
375 client: &InviteLinkAdminClient,
376 org_id: OrganizationId,
377 ) -> (InviteSecret, Invite, B64) {
378 let mut ctx = client.key_store.context();
379 let org_key = SymmetricKeySlotId::Organization(org_id);
380 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
381 let org_public_key = B64::from(
382 ctx.get_public_key(private_key)
383 .unwrap()
384 .to_der()
385 .unwrap()
386 .as_ref(),
387 );
388 let wrapped = ctx.wrap_private_key(org_key, private_key).unwrap();
389 let (secret, invite) = Invite::make_for_private_key(org_key, &wrapped, &mut ctx).unwrap();
390 (secret, invite, org_public_key)
391 }
392
393 #[tokio::test]
398 #[ignore = "Manual test to generate integration-test fixtures"]
399 async fn generate_integration_test_fixtures() {
400 let org_id: OrganizationId = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap();
401 let core = Client::init_test_account(
402 bitwarden_core::client::test_accounts::test_bitwarden_com_account(),
403 )
404 .await;
405 let client = core.invite_link().admin();
406
407 let mut ctx = client.key_store.context();
408 let org_key = SymmetricKeySlotId::Organization(org_id);
409 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
410 let public_key = B64::from(
411 ctx.get_public_key(private_key)
412 .unwrap()
413 .to_der()
414 .unwrap()
415 .as_ref(),
416 );
417 let wrapped = ctx.wrap_private_key(org_key, private_key).unwrap();
418 let (secret, invite) = Invite::make_for_private_key(org_key, &wrapped, &mut ctx).unwrap();
419
420 let mut no_confirmation = invite.clone();
424 no_confirmation.disable_confirmation();
425 assert!(invite.supports_confirmation() && !no_confirmation.supports_confirmation());
426
427 println!("TEST_ORG_WRAPPED_PRIVATE_KEY = {}", wrapped.to_string());
428 println!("TEST_ORG_PUBLIC_KEY = {public_key}");
429 println!("TEST_INVITE = {}", String::from(&invite));
430 println!(
431 "TEST_INVITE_NO_CONFIRMATION = {}",
432 String::from(&no_confirmation)
433 );
434 println!("TEST_INVITE_SECRET = {}", String::from(&secret));
435 }
436
437 #[tokio::test]
438 async fn create_posts_and_returns_link_without_confirmation() {
439 let org_id = OrganizationId::new_v4();
440 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
441 let for_mock = wrapped.clone();
442 let client = make_client(
443 org_id,
444 ApiClient::new_mocked(move |mock| {
445 mock.organizations_api
446 .expect_get_private_key()
447 .returning(move |_org| {
448 Ok(
449 bitwarden_api_api::models::OrganizationPrivateKeyResponseModel {
450 object: None,
451 private_key: for_mock.lock().unwrap().clone(),
452 },
453 )
454 })
455 .once();
456 mock.organization_invite_links_api
457 .expect_create()
458 .returning(|org, model| {
459 let model = model.unwrap();
460 Ok(echo_link_response(
461 org,
462 model.allowed_domains,
463 model.invite,
464 model.supports_confirmation,
465 ))
466 })
467 .once();
468 }),
469 );
470 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
471
472 let link = client
473 .create(org_id, vec!["example.com".to_string()], false)
474 .await
475 .unwrap();
476
477 assert_eq!(link.allowed_domains, vec!["example.com".to_string()]);
478 assert!(!link.supports_confirmation);
479 }
480
481 #[tokio::test]
482 async fn create_posts_and_returns_link_with_confirmation() {
483 let org_id = OrganizationId::new_v4();
484 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
485 let for_mock = wrapped.clone();
486 let client = make_client(
487 org_id,
488 ApiClient::new_mocked(move |mock| {
489 mock.organizations_api
490 .expect_get_private_key()
491 .returning(move |_org| {
492 Ok(
493 bitwarden_api_api::models::OrganizationPrivateKeyResponseModel {
494 object: None,
495 private_key: for_mock.lock().unwrap().clone(),
496 },
497 )
498 })
499 .once();
500 mock.organization_invite_links_api
501 .expect_create()
502 .returning(|org, model| {
503 let model = model.unwrap();
504 Ok(echo_link_response(
505 org,
506 model.allowed_domains,
507 model.invite,
508 model.supports_confirmation,
509 ))
510 })
511 .once();
512 }),
513 );
514 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
515
516 let link = client
517 .create(org_id, vec!["example.com".to_string()], true)
518 .await
519 .unwrap();
520
521 assert_eq!(link.allowed_domains, vec!["example.com".to_string()]);
522 assert!(link.supports_confirmation);
523 }
524
525 #[tokio::test]
526 async fn create_builds_url_fragment_from_org_code_and_secret() {
527 let org_id = OrganizationId::new_v4();
528 let code = uuid::Uuid::new_v4();
529 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
530 let for_mock = wrapped.clone();
531 let client = make_client(
532 org_id,
533 ApiClient::new_mocked(move |mock| {
534 mock.organizations_api
535 .expect_get_private_key()
536 .returning(move |_org| {
537 Ok(
538 bitwarden_api_api::models::OrganizationPrivateKeyResponseModel {
539 object: None,
540 private_key: for_mock.lock().unwrap().clone(),
541 },
542 )
543 })
544 .once();
545 mock.organization_invite_links_api
546 .expect_create()
547 .returning(move |org, model| {
548 let model = model.unwrap();
549 let mut response = echo_link_response(
551 org,
552 model.allowed_domains,
553 model.invite,
554 model.supports_confirmation,
555 );
556 response.code = Some(code);
557 Ok(response)
558 })
559 .once();
560 }),
561 );
562 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
563
564 let link = client
565 .create(org_id, vec!["example.com".to_string()], false)
566 .await
567 .unwrap();
568
569 let prefix = format!("#/join/{org_id}/{code}?key=");
573 let key = link
574 .url_fragment
575 .strip_prefix(&prefix)
576 .unwrap_or_else(|| panic!("unexpected fragment: {}", link.url_fragment));
577 assert!(key.parse::<InviteSecret>().is_ok());
578 }
579
580 #[tokio::test]
581 async fn create_two_calls_produce_different_invites() {
582 let org_id = OrganizationId::new_v4();
583 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
584 let for_mock = wrapped.clone();
585 let client = make_client(
586 org_id,
587 ApiClient::new_mocked(move |mock| {
588 mock.organizations_api
589 .expect_get_private_key()
590 .returning(move |_org| {
591 Ok(
592 bitwarden_api_api::models::OrganizationPrivateKeyResponseModel {
593 object: None,
594 private_key: for_mock.lock().unwrap().clone(),
595 },
596 )
597 })
598 .times(2);
599 mock.organization_invite_links_api
600 .expect_create()
601 .returning(|org, model| {
602 let model = model.unwrap();
603 Ok(echo_link_response(
604 org,
605 model.allowed_domains,
606 model.invite,
607 model.supports_confirmation,
608 ))
609 })
610 .times(2);
611 }),
612 );
613 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
614
615 let link1 = client
616 .create(org_id, vec!["example.com".to_string()], false)
617 .await
618 .unwrap();
619 let link2 = client
620 .create(org_id, vec!["example.com".to_string()], false)
621 .await
622 .unwrap();
623
624 assert_ne!(&link1.url_fragment, &link2.url_fragment);
625 }
626
627 #[tokio::test]
628 async fn create_with_unknown_organization_id_fails() {
629 let org_id = OrganizationId::new_v4();
630 let other_org_id = OrganizationId::new_v4();
631 let wrapped = Arc::new(std::sync::Mutex::new(None::<String>));
632 let for_mock = wrapped.clone();
633 let client = make_client(
634 org_id,
635 ApiClient::new_mocked(move |mock| {
636 mock.organizations_api
637 .expect_get_private_key()
638 .returning(move |_org| {
639 Ok(
640 bitwarden_api_api::models::OrganizationPrivateKeyResponseModel {
641 object: None,
642 private_key: for_mock.lock().unwrap().clone(),
643 },
644 )
645 })
646 .once();
647 }),
648 );
649 *wrapped.lock().unwrap() = Some(wrapped_org_private_key(&client, org_id));
652
653 let result = client
654 .create(other_org_id, vec![String::from("example.com")], false)
655 .await;
656
657 assert!(matches!(result, Err(InviteLinkError::Invite(_))));
658 }
659
660 #[tokio::test]
661 async fn create_surfaces_api_errors() {
662 let org_id = OrganizationId::new_v4();
663 let client = make_client(
664 org_id,
665 ApiClient::new_mocked(|mock| {
666 mock.organizations_api
667 .expect_get_private_key()
668 .returning(|_org| Err(std::io::Error::other("boom").into()));
669 }),
670 );
671
672 let result = client
673 .create(org_id, vec![String::from("example.com")], false)
674 .await;
675
676 assert!(matches!(result, Err(InviteLinkError::Api(_))));
677 }
678
679 #[tokio::test]
680 async fn set_invite_confirmation_enables_confirmation_on_an_existing_invite() {
681 let org_id = OrganizationId::new_v4();
682 let posted = Arc::new(std::sync::Mutex::new(None::<String>));
685 let for_mock = posted.clone();
686 let client = make_client(
687 org_id,
688 ApiClient::new_mocked(move |mock| {
689 mock.organization_invite_links_api
690 .expect_update_invite_support_confirm()
691 .returning(move |org, model| {
692 let model = model.unwrap();
693 *for_mock.lock().unwrap() = Some(model.invite.clone());
694 Ok(echo_link_response(
695 org,
696 vec![],
697 model.invite,
698 model.supports_confirmation,
699 ))
700 })
701 .once();
702 }),
703 );
704
705 let (_secret, mut invite, _org_public_key) = build_invite(&client, org_id);
708 invite.disable_confirmation();
709 assert!(!invite.supports_confirmation());
710
711 let link = client
712 .set_invite_confirmation(org_id, invite, true)
713 .await
714 .unwrap();
715
716 assert!(link.supports_confirmation);
717 assert!(link.invite.supports_confirmation());
718 let posted: Invite = posted.lock().unwrap().clone().unwrap().parse().unwrap();
719 assert!(posted.supports_confirmation());
720 }
721
722 #[tokio::test]
723 async fn set_invite_confirmation_disables_confirmation_on_an_existing_invite() {
724 let org_id = OrganizationId::new_v4();
725 let client = make_client(
726 org_id,
727 ApiClient::new_mocked(|mock| {
728 mock.organization_invite_links_api
729 .expect_update_invite_support_confirm()
730 .returning(|org, model| {
731 let model = model.unwrap();
732 Ok(echo_link_response(
733 org,
734 vec![],
735 model.invite,
736 model.supports_confirmation,
737 ))
738 })
739 .once();
740 }),
741 );
742
743 let (_secret, invite, _org_public_key) = build_invite(&client, org_id);
744 assert!(invite.supports_confirmation());
745
746 let link = client
747 .set_invite_confirmation(org_id, invite, false)
748 .await
749 .unwrap();
750
751 assert!(!link.supports_confirmation);
752 assert!(!link.invite.supports_confirmation());
753 }
754
755 #[tokio::test]
756 async fn set_invite_confirmation_preserves_the_invite_secret() {
757 let org_id = OrganizationId::new_v4();
758 let client = make_client(
759 org_id,
760 ApiClient::new_mocked(|mock| {
761 mock.organization_invite_links_api
762 .expect_update_invite_support_confirm()
763 .returning(|org, model| {
764 let model = model.unwrap();
765 Ok(echo_link_response(
766 org,
767 vec![],
768 model.invite,
769 model.supports_confirmation,
770 ))
771 })
772 .once();
773 }),
774 );
775
776 let (secret, invite, _org_public_key) = build_invite(&client, org_id);
779 let link = client
780 .set_invite_confirmation(org_id, invite, false)
781 .await
782 .unwrap();
783
784 let recovered = client.get_invite_secret(org_id, link.invite).unwrap();
785 assert_eq!(String::from(&recovered), String::from(&secret));
786 }
787
788 #[tokio::test]
789 async fn set_invite_confirmation_with_unknown_organization_id_fails() {
790 let org_id = OrganizationId::new_v4();
791 let other_org_id = OrganizationId::new_v4();
792 let client = make_client(org_id, ApiClient::new_mocked(|_| {}));
793
794 let (_secret, mut invite, _org_public_key) = build_invite(&client, org_id);
797 invite.disable_confirmation();
798
799 let result = client
800 .set_invite_confirmation(other_org_id, invite, true)
801 .await;
802
803 assert!(matches!(result, Err(InviteLinkError::Invite(_))));
804 }
805
806 #[tokio::test]
807 async fn set_invite_confirmation_surfaces_api_errors() {
808 let org_id = OrganizationId::new_v4();
809 let client = make_client(
810 org_id,
811 ApiClient::new_mocked(|mock| {
812 mock.organization_invite_links_api
813 .expect_update_invite_support_confirm()
814 .returning(|_org, _model| Err(std::io::Error::other("boom").into()));
815 }),
816 );
817
818 let (_secret, invite, _org_public_key) = build_invite(&client, org_id);
819 let result = client.set_invite_confirmation(org_id, invite, false).await;
820
821 assert!(matches!(result, Err(InviteLinkError::Api(_))));
822 }
823
824 #[tokio::test]
825 async fn get_returns_none_on_404() {
826 let org_id = OrganizationId::new_v4();
827 let client = make_client(
828 org_id,
829 ApiClient::new_mocked(|mock| {
830 mock.organization_invite_links_api
831 .expect_get()
832 .returning(|_org| {
833 Err(ApiError::Response(ResponseContent {
834 status: StatusCode::NOT_FOUND,
835 message: "not found".to_string(),
836 }))
837 })
838 .once();
839 }),
840 );
841
842 let result = client.get(org_id).await.unwrap();
843
844 assert!(result.is_none());
845 }
846
847 #[tokio::test]
848 async fn get_surfaces_non_404_api_errors() {
849 let org_id = OrganizationId::new_v4();
850 let client = make_client(
851 org_id,
852 ApiClient::new_mocked(|mock| {
853 mock.organization_invite_links_api
854 .expect_get()
855 .returning(|_org| {
856 Err(ApiError::Response(ResponseContent {
857 status: StatusCode::INTERNAL_SERVER_ERROR,
858 message: "boom".to_string(),
859 }))
860 })
861 .once();
862 }),
863 );
864
865 let result = client.get(org_id).await;
866
867 assert!(matches!(result, Err(InviteLinkError::Api(_))));
868 }
869}