1#[cfg(test)]
2use crate::AcquiredCookie;
3use crate::{
4 AcquireCookieError, BootstrapConfig, BootstrapConfigRequest, ServerCommunicationConfig,
5 ServerCommunicationConfigPlatformApi, ServerCommunicationConfigRepository,
6 SetCommunicationTypeRequest, SsoCookieVendorConfig,
7};
8
9pub struct ServerCommunicationConfigClient<R, P>
11where
12 R: ServerCommunicationConfigRepository,
13 P: ServerCommunicationConfigPlatformApi,
14{
15 repository: R,
16 platform_api: P,
17}
18
19impl<R, P> ServerCommunicationConfigClient<R, P>
20where
21 R: ServerCommunicationConfigRepository,
22 P: ServerCommunicationConfigPlatformApi,
23{
24 pub fn new(repository: R, platform_api: P) -> Self {
31 Self {
32 repository,
33 platform_api,
34 }
35 }
36
37 pub async fn get_config(
39 &self,
40 hostname: String,
41 ) -> Result<ServerCommunicationConfig, R::GetError> {
42 Ok(self
43 .repository
44 .get(hostname)
45 .await?
46 .unwrap_or(ServerCommunicationConfig {
47 bootstrap: BootstrapConfig::Direct,
48 }))
49 }
50
51 pub async fn needs_bootstrap(&self, domain: String) -> bool {
58 if let Ok(Some(config)) = self.repository.get(domain).await {
59 return matches!(config.bootstrap, BootstrapConfig::SsoCookieVendor(_));
60 }
61 false
62 }
63
64 pub async fn cookies(&self, hostname: String) -> Vec<(String, String)> {
69 if let Ok(Some(config)) = self.repository.get(hostname).await
70 && let BootstrapConfig::SsoCookieVendor(vendor_config) = config.bootstrap
71 && let Some(acquired_cookies) = vendor_config.cookie_value
72 {
73 return acquired_cookies
74 .into_iter()
75 .map(|cookie| (cookie.name, cookie.value))
76 .collect();
77 }
78 Vec::new()
79 }
80
81 pub async fn set_communication_type(
99 &self,
100 hostname: String,
101 request: SetCommunicationTypeRequest,
102 ) -> Result<(), R::SaveError> {
103 let existing_cookie_value = match &request.bootstrap {
104 BootstrapConfigRequest::SsoCookieVendor(_) => self
105 .repository
106 .get(hostname.clone())
107 .await
108 .ok()
109 .flatten()
110 .and_then(|existing| match existing.bootstrap {
111 BootstrapConfig::SsoCookieVendor(v) => v.cookie_value,
112 _ => None,
113 }),
114 BootstrapConfigRequest::Direct => None,
115 };
116
117 let config = ServerCommunicationConfig {
118 bootstrap: match request.bootstrap {
119 BootstrapConfigRequest::Direct => BootstrapConfig::Direct,
120 BootstrapConfigRequest::SsoCookieVendor(v) => {
121 BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
122 idp_login_url: v.idp_login_url,
123 cookie_name: v.cookie_name,
124 cookie_domain: v.cookie_domain,
125 vault_url: v.vault_url,
126 cookie_value: existing_cookie_value,
127 })
128 }
129 },
130 };
131
132 self.repository.save(hostname, config).await
133 }
134
135 pub async fn acquire_cookie(&self, hostname: &str) -> Result<(), AcquireCookieError> {
155 let mut config = self
157 .repository
158 .get(hostname.to_string())
159 .await
160 .map_err(|e| AcquireCookieError::RepositoryGetError(format!("{:?}", e)))?
161 .ok_or(AcquireCookieError::UnsupportedConfiguration)?;
162
163 let BootstrapConfig::SsoCookieVendor(ref mut vendor_config) = config.bootstrap else {
165 return Err(AcquireCookieError::UnsupportedConfiguration);
166 };
167
168 let expected_cookie_name = vendor_config
169 .cookie_name
170 .as_ref()
171 .ok_or(AcquireCookieError::UnsupportedConfiguration)?;
172
173 let vault_url = vendor_config
175 .vault_url
176 .as_ref()
177 .filter(|s| !s.is_empty())
178 .ok_or(AcquireCookieError::UnsupportedConfiguration)?
179 .clone();
180
181 let cookies = self
183 .platform_api
184 .acquire_cookies(vault_url)
185 .await
186 .ok_or(AcquireCookieError::Cancelled)?;
187
188 let all_cookies_match = cookies.iter().all(|cookie| {
196 cookie.name == *expected_cookie_name
197 || cookie
198 .name
199 .strip_prefix(&format!("{}-", expected_cookie_name))
200 .is_some_and(|suffix| suffix.chars().all(|c| c.is_ascii_digit()))
201 });
202
203 if !all_cookies_match {
204 let mismatched = cookies
206 .iter()
207 .find(|cookie| {
208 cookie.name != *expected_cookie_name
209 && !cookie
210 .name
211 .strip_prefix(&format!("{}-", expected_cookie_name))
212 .is_some_and(|suffix| suffix.chars().all(|c| c.is_ascii_digit()))
213 })
214 .expect("all_cookies_match is false, so at least one cookie must not match");
215
216 return Err(AcquireCookieError::CookieNameMismatch {
217 expected: expected_cookie_name.clone(),
218 actual: mismatched.name.clone(),
219 });
220 }
221
222 vendor_config.cookie_value = Some(cookies);
224
225 self.repository
227 .save(hostname.to_string(), config)
228 .await
229 .map_err(|e| AcquireCookieError::RepositorySaveError(format!("{:?}", e)))?;
230
231 Ok(())
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use std::collections::HashMap;
238
239 use tokio::sync::RwLock;
240
241 use super::*;
242 use crate::{SsoCookieVendorConfig, SsoCookieVendorConfigRequest};
243
244 #[derive(Default, Clone)]
246 struct MockRepository {
247 storage: std::sync::Arc<RwLock<HashMap<String, ServerCommunicationConfig>>>,
248 }
249
250 impl ServerCommunicationConfigRepository for MockRepository {
251 type GetError = ();
252 type SaveError = ();
253
254 async fn get(&self, hostname: String) -> Result<Option<ServerCommunicationConfig>, ()> {
255 Ok(self.storage.read().await.get(&hostname).cloned())
256 }
257
258 async fn save(
259 &self,
260 hostname: String,
261 config: ServerCommunicationConfig,
262 ) -> Result<(), ()> {
263 self.storage.write().await.insert(hostname, config);
264 Ok(())
265 }
266 }
267
268 #[derive(Clone)]
270 struct MockPlatformApi {
271 cookies_to_return: std::sync::Arc<RwLock<Option<Vec<AcquiredCookie>>>>,
272 }
273
274 impl MockPlatformApi {
275 fn new() -> Self {
276 Self {
277 cookies_to_return: std::sync::Arc::new(RwLock::new(None)),
278 }
279 }
280
281 async fn set_cookies(&self, cookies: Option<Vec<AcquiredCookie>>) {
282 *self.cookies_to_return.write().await = cookies;
283 }
284 }
285
286 #[async_trait::async_trait]
287 impl ServerCommunicationConfigPlatformApi for MockPlatformApi {
288 async fn acquire_cookies(&self, _vault_url: String) -> Option<Vec<AcquiredCookie>> {
289 self.cookies_to_return.read().await.clone()
290 }
291 }
292
293 #[tokio::test]
294 async fn get_config_returns_direct_when_not_found() {
295 let repo = MockRepository::default();
296 let platform_api = MockPlatformApi::new();
297 let client = ServerCommunicationConfigClient::new(repo, platform_api);
298
299 let config = client
300 .get_config("vault.example.com".to_string())
301 .await
302 .unwrap();
303
304 assert!(matches!(config.bootstrap, BootstrapConfig::Direct));
305 }
306
307 #[tokio::test]
308 async fn get_config_returns_saved_config() {
309 let repo = MockRepository::default();
310 let config = ServerCommunicationConfig {
311 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
312 idp_login_url: Some("https://example.com".to_string()),
313 cookie_name: Some("TestCookie".to_string()),
314 cookie_domain: Some("example.com".to_string()),
315 vault_url: Some("https://vault.example.com".to_string()),
316 cookie_value: Some(vec![AcquiredCookie {
317 name: "TestCookie".to_string(),
318 value: "value123".to_string(),
319 }]),
320 }),
321 };
322
323 repo.save("vault.example.com".to_string(), config.clone())
324 .await
325 .unwrap();
326
327 let platform_api = MockPlatformApi::new();
328 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
329 let retrieved = client
330 .get_config("vault.example.com".to_string())
331 .await
332 .unwrap();
333
334 assert!(matches!(
335 retrieved.bootstrap,
336 BootstrapConfig::SsoCookieVendor(_)
337 ));
338 }
339
340 #[tokio::test]
341 async fn needs_bootstrap_true_for_sso_cookie_vendor() {
342 let repo = MockRepository::default();
343 let config = ServerCommunicationConfig {
344 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
345 idp_login_url: Some("https://example.com".to_string()),
346 cookie_name: Some("TestCookie".to_string()),
347 cookie_domain: Some("example.com".to_string()),
348 vault_url: Some("https://vault.example.com".to_string()),
349 cookie_value: None,
350 }),
351 };
352
353 repo.save("vault.example.com".to_string(), config)
354 .await
355 .unwrap();
356
357 let platform_api = MockPlatformApi::new();
358 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
359 assert!(
360 client
361 .needs_bootstrap("vault.example.com".to_string())
362 .await
363 );
364 }
365
366 #[tokio::test]
367 async fn needs_bootstrap_true_when_cookie_already_present() {
368 let repo = MockRepository::default();
369 let config = ServerCommunicationConfig {
370 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
371 idp_login_url: Some("https://example.com".to_string()),
372 cookie_name: Some("TestCookie".to_string()),
373 cookie_domain: Some("example.com".to_string()),
374 vault_url: Some("https://vault.example.com".to_string()),
375 cookie_value: Some(vec![AcquiredCookie {
376 name: "TestCookie".to_string(),
377 value: "value123".to_string(),
378 }]),
379 }),
380 };
381
382 repo.save("vault.example.com".to_string(), config)
383 .await
384 .unwrap();
385
386 let platform_api = MockPlatformApi::new();
387 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
388 assert!(
389 client
390 .needs_bootstrap("vault.example.com".to_string())
391 .await
392 );
393 }
394
395 #[tokio::test]
396 async fn needs_bootstrap_false_for_direct() {
397 let repo = MockRepository::default();
398 let config = ServerCommunicationConfig {
399 bootstrap: BootstrapConfig::Direct,
400 };
401
402 repo.save("vault.example.com".to_string(), config)
403 .await
404 .unwrap();
405
406 let platform_api = MockPlatformApi::new();
407 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
408 assert!(
409 !client
410 .needs_bootstrap("vault.example.com".to_string())
411 .await
412 );
413 }
414
415 #[tokio::test]
416 async fn cookies_returns_empty_for_direct() {
417 let repo = MockRepository::default();
418 let config = ServerCommunicationConfig {
419 bootstrap: BootstrapConfig::Direct,
420 };
421
422 repo.save("vault.example.com".to_string(), config)
423 .await
424 .unwrap();
425
426 let platform_api = MockPlatformApi::new();
427 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
428 let cookies = client.cookies("vault.example.com".to_string()).await;
429
430 assert!(cookies.is_empty());
431 }
432
433 #[tokio::test]
434 async fn cookies_returns_empty_when_value_none() {
435 let repo = MockRepository::default();
436 let config = ServerCommunicationConfig {
437 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
438 idp_login_url: Some("https://example.com".to_string()),
439 cookie_name: Some("TestCookie".to_string()),
440 cookie_domain: Some("example.com".to_string()),
441 vault_url: Some("https://vault.example.com".to_string()),
442 cookie_value: None,
443 }),
444 };
445
446 repo.save("vault.example.com".to_string(), config)
447 .await
448 .unwrap();
449
450 let platform_api = MockPlatformApi::new();
451 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
452 let cookies = client.cookies("vault.example.com".to_string()).await;
453
454 assert!(cookies.is_empty());
455 }
456
457 #[tokio::test]
458 async fn cookies_returns_unsharded_cookie_without_suffix() {
459 let repo = MockRepository::default();
460 let config = ServerCommunicationConfig {
461 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
462 idp_login_url: Some("https://example.com".to_string()),
463 cookie_name: Some("AWSELBAuthSessionCookie".to_string()),
464 cookie_domain: Some("example.com".to_string()),
465 vault_url: Some("https://vault.example.com".to_string()),
466 cookie_value: Some(vec![AcquiredCookie {
467 name: "AWSELBAuthSessionCookie".to_string(),
468 value: "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...".to_string(),
469 }]),
470 }),
471 };
472
473 repo.save("vault.example.com".to_string(), config)
474 .await
475 .unwrap();
476
477 let platform_api = MockPlatformApi::new();
478 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
479 let cookies = client.cookies("vault.example.com".to_string()).await;
480
481 assert_eq!(cookies.len(), 1);
483 assert_eq!(cookies[0].0, "AWSELBAuthSessionCookie");
484 assert_eq!(cookies[0].1, "eyJhbGciOiJFUzI1NiIsImtpZCI6Im...");
485 }
486
487 #[tokio::test]
488 async fn cookies_returns_empty_when_no_config() {
489 let repo = MockRepository::default();
490 let platform_api = MockPlatformApi::new();
491 let client = ServerCommunicationConfigClient::new(repo, platform_api);
492 let cookies = client.cookies("vault.example.com".to_string()).await;
493
494 assert!(cookies.is_empty());
495 }
496
497 #[tokio::test]
498 async fn cookies_returns_shards_with_numbered_suffixes() {
499 let repo = MockRepository::default();
500 let config = ServerCommunicationConfig {
501 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
502 idp_login_url: Some("https://example.com".to_string()),
503 cookie_name: Some("AWSELBAuthSessionCookie".to_string()),
504 cookie_domain: Some("example.com".to_string()),
505 vault_url: Some("https://vault.example.com".to_string()),
506 cookie_value: Some(vec![
507 AcquiredCookie {
508 name: "AWSELBAuthSessionCookie-0".to_string(),
509 value: "shard0value".to_string(),
510 },
511 AcquiredCookie {
512 name: "AWSELBAuthSessionCookie-1".to_string(),
513 value: "shard1value".to_string(),
514 },
515 AcquiredCookie {
516 name: "AWSELBAuthSessionCookie-2".to_string(),
517 value: "shard2value".to_string(),
518 },
519 ]),
520 }),
521 };
522
523 repo.save("vault.example.com".to_string(), config)
524 .await
525 .unwrap();
526
527 let platform_api = MockPlatformApi::new();
528 let client = ServerCommunicationConfigClient::new(repo, platform_api);
529 let cookies = client.cookies("vault.example.com".to_string()).await;
530
531 assert_eq!(cookies.len(), 3);
533 assert_eq!(
534 cookies[0],
535 (
536 "AWSELBAuthSessionCookie-0".to_string(),
537 "shard0value".to_string()
538 )
539 );
540 assert_eq!(
541 cookies[1],
542 (
543 "AWSELBAuthSessionCookie-1".to_string(),
544 "shard1value".to_string()
545 )
546 );
547 assert_eq!(
548 cookies[2],
549 (
550 "AWSELBAuthSessionCookie-2".to_string(),
551 "shard2value".to_string()
552 )
553 );
554 }
555
556 #[tokio::test]
557 async fn acquire_cookie_saves_when_cookie_returned() {
558 let repo = MockRepository::default();
559 let platform_api = MockPlatformApi::new();
560
561 let config = ServerCommunicationConfig {
563 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
564 idp_login_url: Some("https://example.com".to_string()),
565 cookie_name: Some("TestCookie".to_string()),
566 cookie_domain: Some("example.com".to_string()),
567 vault_url: Some("https://vault.example.com".to_string()),
568 cookie_value: None,
569 }),
570 };
571 repo.save("vault.example.com".to_string(), config)
572 .await
573 .unwrap();
574
575 platform_api
577 .set_cookies(Some(vec![AcquiredCookie {
578 name: "TestCookie".to_string(),
579 value: "acquired-cookie-value".to_string(),
580 }]))
581 .await;
582
583 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
584
585 client.acquire_cookie("vault.example.com").await.unwrap();
587
588 let saved_config = repo
590 .get("vault.example.com".to_string())
591 .await
592 .unwrap()
593 .unwrap();
594
595 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
596 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 1);
597 assert_eq!(
598 vendor_config.cookie_value.as_ref().unwrap()[0].name,
599 "TestCookie"
600 );
601 assert_eq!(
602 vendor_config.cookie_value.as_ref().unwrap()[0].value,
603 "acquired-cookie-value"
604 );
605 } else {
606 panic!("Expected SsoCookieVendor config");
607 }
608 }
609
610 #[tokio::test]
611 async fn acquire_cookie_returns_cancelled_when_none() {
612 let repo = MockRepository::default();
613 let platform_api = MockPlatformApi::new();
614
615 let config = ServerCommunicationConfig {
617 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
618 idp_login_url: Some("https://example.com".to_string()),
619 cookie_name: Some("TestCookie".to_string()),
620 cookie_domain: Some("example.com".to_string()),
621 vault_url: Some("https://vault.example.com".to_string()),
622 cookie_value: None,
623 }),
624 };
625 repo.save("vault.example.com".to_string(), config)
626 .await
627 .unwrap();
628
629 platform_api.set_cookies(None).await;
631
632 let client = ServerCommunicationConfigClient::new(repo, platform_api);
633
634 let result = client.acquire_cookie("vault.example.com").await;
635
636 assert!(matches!(result, Err(AcquireCookieError::Cancelled)));
637 }
638
639 #[tokio::test]
640 async fn acquire_cookie_returns_unsupported_for_direct_config() {
641 let repo = MockRepository::default();
642 let platform_api = MockPlatformApi::new();
643
644 let config = ServerCommunicationConfig {
646 bootstrap: BootstrapConfig::Direct,
647 };
648 repo.save("vault.example.com".to_string(), config)
649 .await
650 .unwrap();
651
652 platform_api
654 .set_cookies(Some(vec![AcquiredCookie {
655 name: "TestCookie".to_string(),
656 value: "cookie-value".to_string(),
657 }]))
658 .await;
659
660 let client = ServerCommunicationConfigClient::new(repo, platform_api);
661
662 let result = client.acquire_cookie("vault.example.com").await;
663
664 assert!(matches!(
666 result,
667 Err(AcquireCookieError::UnsupportedConfiguration)
668 ));
669 }
670
671 #[tokio::test]
672 async fn acquire_cookie_validates_cookie_name() {
673 let repo = MockRepository::default();
674 let platform_api = MockPlatformApi::new();
675
676 let config = ServerCommunicationConfig {
678 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
679 idp_login_url: Some("https://example.com".to_string()),
680 cookie_name: Some("ExpectedCookie".to_string()),
681 cookie_domain: Some("example.com".to_string()),
682 vault_url: Some("https://vault.example.com".to_string()),
683 cookie_value: None,
684 }),
685 };
686 repo.save("vault.example.com".to_string(), config)
687 .await
688 .unwrap();
689
690 platform_api
692 .set_cookies(Some(vec![AcquiredCookie {
693 name: "WrongCookie".to_string(),
694 value: "some-value".to_string(),
695 }]))
696 .await;
697
698 let client = ServerCommunicationConfigClient::new(repo, platform_api);
699
700 let result = client.acquire_cookie("vault.example.com").await;
701
702 match result {
704 Err(AcquireCookieError::CookieNameMismatch { expected, actual }) => {
705 assert_eq!(expected, "ExpectedCookie");
706 assert_eq!(actual, "WrongCookie");
707 }
708 _ => panic!("Expected CookieNameMismatch error"),
709 }
710 }
711
712 #[tokio::test]
713 async fn acquire_cookie_returns_unsupported_when_no_config() {
714 let repo = MockRepository::default();
715 let platform_api = MockPlatformApi::new();
716
717 let client = ServerCommunicationConfigClient::new(repo, platform_api);
720
721 let result = client.acquire_cookie("vault.example.com").await;
722
723 assert!(matches!(
725 result,
726 Err(AcquireCookieError::UnsupportedConfiguration)
727 ));
728 }
729
730 #[tokio::test]
731 async fn acquire_cookie_accepts_sharded_cookies_with_numbered_suffixes() {
732 let repo = MockRepository::default();
733 let platform_api = MockPlatformApi::new();
734
735 let config = ServerCommunicationConfig {
737 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
738 idp_login_url: Some("https://example.com".to_string()),
739 cookie_name: Some("AWSELBAuthSessionCookie".to_string()),
740 cookie_domain: Some("example.com".to_string()),
741 vault_url: Some("https://vault.example.com".to_string()),
742 cookie_value: None,
743 }),
744 };
745 repo.save("vault.example.com".to_string(), config)
746 .await
747 .unwrap();
748
749 platform_api
751 .set_cookies(Some(vec![
752 AcquiredCookie {
753 name: "AWSELBAuthSessionCookie-0".to_string(),
754 value: "shard0value".to_string(),
755 },
756 AcquiredCookie {
757 name: "AWSELBAuthSessionCookie-1".to_string(),
758 value: "shard1value".to_string(),
759 },
760 AcquiredCookie {
761 name: "AWSELBAuthSessionCookie-2".to_string(),
762 value: "shard2value".to_string(),
763 },
764 ]))
765 .await;
766
767 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
768
769 client.acquire_cookie("vault.example.com").await.unwrap();
771
772 let saved_config = repo
774 .get("vault.example.com".to_string())
775 .await
776 .unwrap()
777 .unwrap();
778
779 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
780 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 3);
781 assert_eq!(
782 vendor_config.cookie_value.as_ref().unwrap()[0].name,
783 "AWSELBAuthSessionCookie-0"
784 );
785 assert_eq!(
786 vendor_config.cookie_value.as_ref().unwrap()[1].name,
787 "AWSELBAuthSessionCookie-1"
788 );
789 assert_eq!(
790 vendor_config.cookie_value.as_ref().unwrap()[2].name,
791 "AWSELBAuthSessionCookie-2"
792 );
793 } else {
794 panic!("Expected SsoCookieVendor config");
795 }
796 }
797
798 #[tokio::test]
799 async fn acquire_cookie_accepts_unsharded_cookie_without_suffix() {
800 let repo = MockRepository::default();
801 let platform_api = MockPlatformApi::new();
802
803 let config = ServerCommunicationConfig {
805 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
806 idp_login_url: Some("https://example.com".to_string()),
807 cookie_name: Some("SessionCookie".to_string()),
808 cookie_domain: Some("example.com".to_string()),
809 vault_url: Some("https://vault.example.com".to_string()),
810 cookie_value: None,
811 }),
812 };
813 repo.save("vault.example.com".to_string(), config)
814 .await
815 .unwrap();
816
817 platform_api
819 .set_cookies(Some(vec![AcquiredCookie {
820 name: "SessionCookie".to_string(),
821 value: "single-cookie-value".to_string(),
822 }]))
823 .await;
824
825 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
826
827 client.acquire_cookie("vault.example.com").await.unwrap();
829
830 let saved_config = repo
832 .get("vault.example.com".to_string())
833 .await
834 .unwrap()
835 .unwrap();
836
837 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
838 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 1);
839 assert_eq!(
840 vendor_config.cookie_value.as_ref().unwrap()[0].name,
841 "SessionCookie"
842 );
843 assert_eq!(
844 vendor_config.cookie_value.as_ref().unwrap()[0].value,
845 "single-cookie-value"
846 );
847 } else {
848 panic!("Expected SsoCookieVendor config");
849 }
850 }
851
852 #[tokio::test]
853 async fn set_communication_type_saves_direct_config() {
854 let repo = MockRepository::default();
855 let platform_api = MockPlatformApi::new();
856 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
857
858 let request = SetCommunicationTypeRequest {
859 bootstrap: BootstrapConfigRequest::Direct,
860 };
861
862 client
863 .set_communication_type("vault.example.com".to_string(), request)
864 .await
865 .unwrap();
866
867 let saved_config = repo
868 .get("vault.example.com".to_string())
869 .await
870 .unwrap()
871 .unwrap();
872
873 assert!(matches!(saved_config.bootstrap, BootstrapConfig::Direct));
874 }
875
876 #[tokio::test]
877 async fn set_communication_type_saves_sso_cookie_vendor_config() {
878 let repo = MockRepository::default();
879 let platform_api = MockPlatformApi::new();
880 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
881
882 let request = SetCommunicationTypeRequest {
883 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
884 idp_login_url: Some("https://idp.example.com/login".to_string()),
885 cookie_name: Some("SessionCookie".to_string()),
886 cookie_domain: Some("vault.example.com".to_string()),
887 vault_url: Some("https://vault.example.com".to_string()),
888 }),
889 };
890
891 client
892 .set_communication_type("vault.example.com".to_string(), request)
893 .await
894 .unwrap();
895
896 let saved_config = repo
897 .get("vault.example.com".to_string())
898 .await
899 .unwrap()
900 .unwrap();
901
902 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
903 assert_eq!(
904 vendor_config.idp_login_url,
905 Some("https://idp.example.com/login".to_string())
906 );
907 assert_eq!(vendor_config.cookie_name, Some("SessionCookie".to_string()));
908 assert_eq!(
909 vendor_config.cookie_domain,
910 Some("vault.example.com".to_string())
911 );
912 assert!(vendor_config.cookie_value.is_none());
913 } else {
914 panic!("Expected SsoCookieVendor config");
915 }
916 }
917
918 #[tokio::test]
919 async fn set_communication_type_overwrites_existing_config() {
920 let repo = MockRepository::default();
921 let platform_api = MockPlatformApi::new();
922
923 let old_config = ServerCommunicationConfig {
925 bootstrap: BootstrapConfig::Direct,
926 };
927 repo.save("vault.example.com".to_string(), old_config)
928 .await
929 .unwrap();
930
931 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
932
933 let request = SetCommunicationTypeRequest {
935 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
936 idp_login_url: Some("https://new-idp.example.com/login".to_string()),
937 cookie_name: Some("NewCookie".to_string()),
938 cookie_domain: Some("vault.example.com".to_string()),
939 vault_url: Some("https://vault.example.com".to_string()),
940 }),
941 };
942
943 client
944 .set_communication_type("vault.example.com".to_string(), request)
945 .await
946 .unwrap();
947
948 let saved_config = repo
949 .get("vault.example.com".to_string())
950 .await
951 .unwrap()
952 .unwrap();
953
954 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
955 assert_eq!(
956 vendor_config.idp_login_url,
957 Some("https://new-idp.example.com/login".to_string())
958 );
959 assert_eq!(vendor_config.cookie_name, Some("NewCookie".to_string()));
960 } else {
961 panic!("Expected SsoCookieVendor config");
962 }
963 }
964
965 #[tokio::test]
966 async fn set_communication_type_preserves_per_hostname_isolation() {
967 let repo = MockRepository::default();
968 let platform_api = MockPlatformApi::new();
969 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
970
971 let request1 = SetCommunicationTypeRequest {
972 bootstrap: BootstrapConfigRequest::Direct,
973 };
974 client
975 .set_communication_type("vault1.example.com".to_string(), request1)
976 .await
977 .unwrap();
978
979 let request2 = SetCommunicationTypeRequest {
980 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
981 idp_login_url: Some("https://idp.example.com/login".to_string()),
982 cookie_name: Some("TestCookie".to_string()),
983 cookie_domain: Some("vault2.example.com".to_string()),
984 vault_url: Some("https://vault2.example.com".to_string()),
985 }),
986 };
987 client
988 .set_communication_type("vault2.example.com".to_string(), request2)
989 .await
990 .unwrap();
991
992 let saved_config1 = repo
993 .get("vault1.example.com".to_string())
994 .await
995 .unwrap()
996 .unwrap();
997 assert!(matches!(saved_config1.bootstrap, BootstrapConfig::Direct));
998
999 let saved_config2 = repo
1000 .get("vault2.example.com".to_string())
1001 .await
1002 .unwrap()
1003 .unwrap();
1004 assert!(matches!(
1005 saved_config2.bootstrap,
1006 BootstrapConfig::SsoCookieVendor(_)
1007 ));
1008 }
1009
1010 #[tokio::test]
1011 async fn set_communication_type_preserves_existing_cookies() {
1012 let repo = MockRepository::default();
1013 let platform_api = MockPlatformApi::new();
1014
1015 let existing_config = ServerCommunicationConfig {
1017 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1018 idp_login_url: Some("https://idp.example.com/login".to_string()),
1019 cookie_name: Some("SessionCookie".to_string()),
1020 cookie_domain: Some("example.com".to_string()),
1021 vault_url: Some("https://vault.example.com".to_string()),
1022 cookie_value: Some(vec![AcquiredCookie {
1023 name: "SessionCookie".to_string(),
1024 value: "previously-acquired-value".to_string(),
1025 }]),
1026 }),
1027 };
1028 repo.save("vault.example.com".to_string(), existing_config)
1029 .await
1030 .unwrap();
1031
1032 let client = ServerCommunicationConfigClient::new(repo.clone(), platform_api);
1033
1034 let request = SetCommunicationTypeRequest {
1036 bootstrap: BootstrapConfigRequest::SsoCookieVendor(SsoCookieVendorConfigRequest {
1037 idp_login_url: Some("https://idp.example.com/login".to_string()),
1038 cookie_name: Some("SessionCookie".to_string()),
1039 cookie_domain: Some("example.com".to_string()),
1040 vault_url: Some("https://vault.example.com".to_string()),
1041 }),
1042 };
1043
1044 client
1045 .set_communication_type("vault.example.com".to_string(), request)
1046 .await
1047 .unwrap();
1048
1049 let saved_config = repo
1051 .get("vault.example.com".to_string())
1052 .await
1053 .unwrap()
1054 .unwrap();
1055
1056 if let BootstrapConfig::SsoCookieVendor(vendor_config) = saved_config.bootstrap {
1057 let cookies = vendor_config
1058 .cookie_value
1059 .expect("cookies should be preserved");
1060 assert_eq!(cookies.len(), 1);
1061 assert_eq!(cookies[0].name, "SessionCookie");
1062 assert_eq!(cookies[0].value, "previously-acquired-value");
1063 } else {
1064 panic!("Expected SsoCookieVendor config");
1065 }
1066 }
1067
1068 #[tokio::test]
1069 async fn acquire_cookie_returns_unsupported_when_vault_url_none() {
1070 let repo = MockRepository::default();
1071 let platform_api = MockPlatformApi::new();
1072
1073 let config = ServerCommunicationConfig {
1075 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1076 idp_login_url: Some("https://example.com".to_string()),
1077 cookie_name: Some("TestCookie".to_string()),
1078 cookie_domain: Some("example.com".to_string()),
1079 vault_url: None, cookie_value: None,
1081 }),
1082 };
1083 repo.save("vault.example.com".to_string(), config)
1084 .await
1085 .unwrap();
1086
1087 platform_api
1089 .set_cookies(Some(vec![AcquiredCookie {
1090 name: "TestCookie".to_string(),
1091 value: "value".to_string(),
1092 }]))
1093 .await;
1094
1095 let client = ServerCommunicationConfigClient::new(repo, platform_api);
1096
1097 let result = client.acquire_cookie("vault.example.com").await;
1098
1099 assert!(matches!(
1101 result,
1102 Err(AcquireCookieError::UnsupportedConfiguration)
1103 ));
1104 }
1105
1106 #[tokio::test]
1107 async fn acquire_cookie_returns_unsupported_when_vault_url_empty() {
1108 let repo = MockRepository::default();
1109 let platform_api = MockPlatformApi::new();
1110
1111 let config = ServerCommunicationConfig {
1113 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
1114 idp_login_url: Some("https://example.com".to_string()),
1115 cookie_name: Some("TestCookie".to_string()),
1116 cookie_domain: Some("example.com".to_string()),
1117 vault_url: Some("".to_string()), cookie_value: None,
1119 }),
1120 };
1121 repo.save("vault.example.com".to_string(), config)
1122 .await
1123 .unwrap();
1124
1125 platform_api
1127 .set_cookies(Some(vec![AcquiredCookie {
1128 name: "TestCookie".to_string(),
1129 value: "value".to_string(),
1130 }]))
1131 .await;
1132
1133 let client = ServerCommunicationConfigClient::new(repo, platform_api);
1134
1135 let result = client.acquire_cookie("vault.example.com").await;
1136
1137 assert!(matches!(
1139 result,
1140 Err(AcquireCookieError::UnsupportedConfiguration)
1141 ));
1142 }
1143}