1use crate::storage::{EncryptedFilesystemStorage, StorageBackend, StorageError};
2use chrono::{DateTime, Utc};
3use serde::{de::DeserializeOwned, Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct MachineToken {
11 pub machine_token: String,
13 pub expires_at: String,
15 pub gateway_id: String,
17 pub gateway_code: String,
19 #[serde(default)]
21 pub abilities: Vec<String>,
22 pub issued_at: String,
24}
25
26impl MachineToken {
27 pub fn new(
29 machine_token: String,
30 expires_at: String,
31 gateway_id: String,
32 gateway_code: String,
33 abilities: Vec<String>,
34 ) -> Self {
35 let issued_at = Utc::now().to_rfc3339();
36
37 Self {
38 machine_token,
39 expires_at,
40 gateway_id,
41 gateway_code,
42 abilities,
43 issued_at,
44 }
45 }
46
47 pub fn is_expired(&self) -> bool {
49 match DateTime::parse_from_rfc3339(&self.expires_at) {
51 Ok(expiry) => {
52 let now = Utc::now();
53 expiry.with_timezone(&Utc) < now
54 }
55 Err(e) => {
56 tracing::warn!("Failed to parse token expiry date: {}", e);
57 true
59 }
60 }
61 }
62
63 pub fn is_valid(&self) -> bool {
65 !self.is_expired()
66 }
67}
68
69pub async fn save_token<T>(
123 instance_id: &str,
124 token_type: &str,
125 token: &T,
126) -> Result<(), StorageError>
127where
128 T: Serialize,
129{
130 let token_path = format!("runbeam/{}.json", token_type);
131 tracing::debug!(
132 "Saving token: type={}, instance={}, path={}",
133 token_type,
134 instance_id,
135 token_path
136 );
137
138 let storage = EncryptedFilesystemStorage::new_with_instance(instance_id)
140 .await
141 .map_err(|e| {
142 tracing::error!("Failed to initialize encrypted storage: {}", e);
143 e
144 })?;
145
146 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
148 tracing::error!("Failed to serialize token: {}", e);
149 StorageError::Config(format!("JSON serialization failed: {}", e))
150 })?;
151
152 storage.write_file_str(&token_path, &json).await?;
154
155 tracing::info!(
156 "Token saved successfully to encrypted storage: type={}, instance={}",
157 token_type,
158 instance_id
159 );
160
161 Ok(())
162}
163
164pub async fn load_token<T>(instance_id: &str, token_type: &str) -> Result<Option<T>, StorageError>
198where
199 T: DeserializeOwned,
200{
201 let token_path = format!("runbeam/{}.json", token_type);
202 tracing::debug!(
203 "Loading token: type={}, instance={}, path={}",
204 token_type,
205 instance_id,
206 token_path
207 );
208
209 let storage = EncryptedFilesystemStorage::new_with_instance(instance_id)
211 .await
212 .map_err(|e| {
213 tracing::debug!("Failed to initialize encrypted storage: {}", e);
214 e
215 })?;
216
217 if !storage.exists_str(&token_path) {
219 tracing::debug!("No token file found: type={}", token_type);
220 return Ok(None);
221 }
222
223 tracing::debug!("Token found in encrypted filesystem, loading...");
225 let json = storage.read_file_str(&token_path).await?;
226
227 let token: T = serde_json::from_slice(&json).map_err(|e| {
229 tracing::error!("Failed to deserialize token: {}", e);
230 StorageError::Config(format!("JSON deserialization failed: {}", e))
231 })?;
232
233 tracing::debug!(
234 "Token loaded successfully from encrypted filesystem: type={}",
235 token_type
236 );
237 Ok(Some(token))
238}
239
240pub async fn clear_token(instance_id: &str, token_type: &str) -> Result<(), StorageError> {
269 let token_path = format!("runbeam/{}.json", token_type);
270 tracing::debug!(
271 "Clearing token: type={}, instance={}, path={}",
272 token_type,
273 instance_id,
274 token_path
275 );
276
277 let storage = EncryptedFilesystemStorage::new_with_instance(instance_id)
279 .await
280 .map_err(|e| {
281 tracing::debug!("Failed to initialize encrypted storage: {}", e);
282 e
283 })?;
284
285 if !storage.exists_str(&token_path) {
287 tracing::debug!("No token file to clear: type={}", token_type);
288 return Ok(());
289 }
290
291 tracing::debug!("Clearing token from encrypted filesystem storage");
293 storage.remove_str(&token_path).await.map_err(|e| {
294 tracing::error!("Failed to clear token: {}", e);
295 e
296 })?;
297
298 tracing::info!("Token cleared successfully: type={}", token_type);
299 Ok(())
300}
301
302pub async fn save_token_with_key(
321 instance_id: &str,
322 token: &MachineToken,
323 encryption_key: &str,
324) -> Result<(), StorageError> {
325 let token_path = "runbeam/auth.json";
326 tracing::debug!(
327 "Saving machine token with explicit encryption key: gateway={}, instance={}",
328 token.gateway_code,
329 instance_id
330 );
331
332 let storage =
334 EncryptedFilesystemStorage::new_with_instance_and_key(instance_id, encryption_key).await?;
335
336 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
338 tracing::error!("Failed to serialize machine token: {}", e);
339 StorageError::Config(format!("JSON serialization failed: {}", e))
340 })?;
341
342 storage.write_file_str(token_path, &json).await?;
344
345 tracing::info!(
346 "Machine token saved successfully with explicit key: gateway_id={}, expires_at={}",
347 token.gateway_id,
348 token.expires_at
349 );
350
351 Ok(())
352}
353
354pub async fn save_machine_token(
376 instance_id: &str,
377 token: &MachineToken,
378) -> Result<(), StorageError> {
379 save_token(instance_id, "auth", token).await
380}
381
382pub async fn load_machine_token(instance_id: &str) -> Result<Option<MachineToken>, StorageError> {
398 load_token(instance_id, "auth").await
399}
400
401pub async fn clear_machine_token(instance_id: &str) -> Result<(), StorageError> {
416 clear_token(instance_id, "auth").await
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use serial_test::serial;
423
424 fn setup_test_encryption() -> impl Drop {
426 use base64::Engine;
427 use secrecy::ExposeSecret;
428 use std::env;
429
430 let identity = age::x25519::Identity::generate();
431 let key_base64 = base64::engine::general_purpose::STANDARD
432 .encode(identity.to_string().expose_secret().as_bytes());
433 env::set_var("RUNBEAM_ENCRYPTION_KEY", &key_base64);
434
435 struct Guard;
437 impl Drop for Guard {
438 fn drop(&mut self) {
439 std::env::remove_var("RUNBEAM_ENCRYPTION_KEY");
440 }
441 }
442 Guard
443 }
444
445 #[test]
446 fn test_machine_token_creation() {
447 let token = MachineToken::new(
448 "test_token".to_string(),
449 "2025-12-31T23:59:59Z".to_string(),
450 "gw123".to_string(),
451 "gateway-code-123".to_string(),
452 vec!["harmony:send".to_string(), "harmony:receive".to_string()],
453 );
454
455 assert_eq!(token.machine_token, "test_token");
456 assert_eq!(token.gateway_id, "gw123");
457 assert_eq!(token.gateway_code, "gateway-code-123");
458 assert_eq!(token.abilities.len(), 2);
459 assert!(!token.issued_at.is_empty());
460 }
461
462 #[test]
463 fn test_machine_token_is_expired() {
464 let expired_token = MachineToken::new(
466 "test_token".to_string(),
467 "2020-01-01T00:00:00Z".to_string(),
468 "gw123".to_string(),
469 "gateway-code-123".to_string(),
470 vec![],
471 );
472 assert!(expired_token.is_expired());
473 assert!(!expired_token.is_valid());
474
475 let valid_token = MachineToken::new(
477 "test_token".to_string(),
478 "2099-12-31T23:59:59Z".to_string(),
479 "gw123".to_string(),
480 "gateway-code-123".to_string(),
481 vec![],
482 );
483 assert!(!valid_token.is_expired());
484 assert!(valid_token.is_valid());
485 }
486
487 #[test]
488 fn test_machine_token_serialization() {
489 let token = MachineToken::new(
490 "test_token".to_string(),
491 "2025-12-31T23:59:59Z".to_string(),
492 "gw123".to_string(),
493 "gateway-code-123".to_string(),
494 vec!["harmony:send".to_string()],
495 );
496
497 let json = serde_json::to_string(&token).unwrap();
498 assert!(json.contains("\"machine_token\":\"test_token\""));
499 assert!(json.contains("\"gateway_id\":\"gw123\""));
500 assert!(json.contains("\"gateway_code\":\"gateway-code-123\""));
501
502 let deserialized: MachineToken = serde_json::from_str(&json).unwrap();
504 assert_eq!(deserialized.machine_token, token.machine_token);
505 assert_eq!(deserialized.gateway_id, token.gateway_id);
506 }
507
508 #[tokio::test]
509 #[serial]
510 async fn test_save_and_load_token_secure() {
511 let _guard = setup_test_encryption();
512 let instance_id = "test-save-load";
513 let _ = clear_machine_token(instance_id).await;
515
516 let token = MachineToken::new(
517 "test_token_secure".to_string(),
518 "2099-12-31T23:59:59Z".to_string(),
519 "gw_test".to_string(),
520 "test-gateway".to_string(),
521 vec!["harmony:send".to_string()],
522 );
523
524 save_machine_token(instance_id, &token).await.unwrap();
526
527 let loaded = load_machine_token(instance_id).await.unwrap();
529 assert!(loaded.is_some());
530
531 let loaded_token = loaded.unwrap();
532 assert_eq!(loaded_token.machine_token, token.machine_token);
533 assert_eq!(loaded_token.gateway_id, token.gateway_id);
534 assert_eq!(loaded_token.gateway_code, token.gateway_code);
535 assert!(loaded_token.is_valid());
536
537 clear_machine_token(instance_id).await.unwrap();
539 }
540
541 #[tokio::test]
542 #[serial]
543 async fn test_load_nonexistent_token_secure() {
544 let _guard = setup_test_encryption();
545 let instance_id = "test-nonexistent";
546 let _ = clear_machine_token(instance_id).await;
548
549 let result = load_machine_token(instance_id).await.unwrap();
551 assert!(result.is_none());
552 }
553
554 #[tokio::test]
555 #[serial]
556 async fn test_clear_token_secure() {
557 let _guard = setup_test_encryption();
558 let instance_id = "test-clear";
559 let _ = clear_machine_token(instance_id).await;
561
562 let token = MachineToken::new(
563 "test_clear".to_string(),
564 "2099-12-31T23:59:59Z".to_string(),
565 "gw_clear".to_string(),
566 "clear-test".to_string(),
567 vec![],
568 );
569
570 save_machine_token(instance_id, &token).await.unwrap();
572
573 assert!(load_machine_token(instance_id).await.unwrap().is_some());
575
576 clear_machine_token(instance_id).await.unwrap();
578
579 assert!(load_machine_token(instance_id).await.unwrap().is_none());
581 }
582
583 #[tokio::test]
584 #[serial]
585 async fn test_clear_nonexistent_token_secure() {
586 let _guard = setup_test_encryption();
587 let instance_id = "test-clear-nonexistent";
588 clear_machine_token(instance_id).await.unwrap();
590 }
591
592 #[tokio::test]
593 #[serial]
594 async fn test_token_expiry_detection() {
595 let _guard = setup_test_encryption();
596 let instance_id = "test-expiry";
597 let _ = clear_machine_token(instance_id).await;
598
599 let expired_token = MachineToken::new(
601 "expired_token".to_string(),
602 "2020-01-01T00:00:00Z".to_string(),
603 "gw_expired".to_string(),
604 "expired-gateway".to_string(),
605 vec![],
606 );
607
608 save_machine_token(instance_id, &expired_token)
609 .await
610 .unwrap();
611
612 let loaded = load_machine_token(instance_id).await.unwrap();
614 assert!(loaded.is_some());
615 let loaded_token = loaded.unwrap();
616 assert!(loaded_token.is_expired());
617 assert!(!loaded_token.is_valid());
618
619 clear_machine_token(instance_id).await.unwrap();
621 }
622
623 #[tokio::test]
624 #[serial]
625 async fn test_token_with_abilities() {
626 let _guard = setup_test_encryption();
627 let instance_id = "test-abilities";
628 let _ = clear_machine_token(instance_id).await;
629
630 let token = MachineToken::new(
631 "token_with_abilities".to_string(),
632 "2099-12-31T23:59:59Z".to_string(),
633 "gw_abilities".to_string(),
634 "abilities-test".to_string(),
635 vec![
636 "harmony:send".to_string(),
637 "harmony:receive".to_string(),
638 "harmony:config".to_string(),
639 ],
640 );
641
642 save_machine_token(instance_id, &token).await.unwrap();
643
644 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
645 assert_eq!(loaded.abilities.len(), 3);
646 assert!(loaded.abilities.contains(&"harmony:send".to_string()));
647 assert!(loaded.abilities.contains(&"harmony:receive".to_string()));
648 assert!(loaded.abilities.contains(&"harmony:config".to_string()));
649
650 clear_machine_token(instance_id).await.unwrap();
652 }
653
654 #[tokio::test]
655 #[serial]
656 async fn test_token_overwrites_existing() {
657 let _guard = setup_test_encryption();
658 let instance_id = "test-overwrite";
659 let _ = clear_machine_token(instance_id).await;
660
661 let token1 = MachineToken::new(
663 "first_token".to_string(),
664 "2099-12-31T23:59:59Z".to_string(),
665 "gw_first".to_string(),
666 "first-gateway".to_string(),
667 vec![],
668 );
669 save_machine_token(instance_id, &token1).await.unwrap();
670
671 let token2 = MachineToken::new(
673 "second_token".to_string(),
674 "2099-12-31T23:59:59Z".to_string(),
675 "gw_second".to_string(),
676 "second-gateway".to_string(),
677 vec![],
678 );
679 save_machine_token(instance_id, &token2).await.unwrap();
680
681 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
683 assert_eq!(loaded.machine_token, "second_token");
684 assert_eq!(loaded.gateway_id, "gw_second");
685 assert_eq!(loaded.gateway_code, "second-gateway");
686
687 clear_machine_token(instance_id).await.unwrap();
689 }
690
691 #[tokio::test]
692 async fn test_token_encrypted_on_disk() {
693 use crate::storage::EncryptedFilesystemStorage;
694 use tempfile::TempDir;
695
696 let instance_id = "test-encryption-verify";
697 let temp_dir = TempDir::new().unwrap();
698
699 let token = MachineToken::new(
701 "super_secret_token_12345".to_string(),
702 "2099-12-31T23:59:59Z".to_string(),
703 "gw_secret".to_string(),
704 "secret-gateway".to_string(),
705 vec!["harmony:admin".to_string()],
706 );
707
708 let storage_path = temp_dir.path().join(instance_id);
710 let storage = EncryptedFilesystemStorage::new(&storage_path)
711 .await
712 .unwrap();
713
714 let token_json = serde_json::to_vec(&token).unwrap();
716 storage
717 .write_file_str("auth.json", &token_json)
718 .await
719 .unwrap();
720
721 let token_path = storage_path.join("auth.json");
723
724 assert!(
726 token_path.exists(),
727 "Token file should exist at {:?}",
728 token_path
729 );
730
731 let raw_contents = std::fs::read(&token_path).unwrap();
733 let raw_string = String::from_utf8_lossy(&raw_contents);
734
735 assert!(
737 !raw_string.contains("super_secret_token_12345"),
738 "Token file should NOT contain plaintext token: {}",
739 raw_string
740 );
741 assert!(
742 !raw_string.contains("gw_secret"),
743 "Token file should NOT contain plaintext gateway_id: {}",
744 raw_string
745 );
746 assert!(
747 !raw_string.contains("secret-gateway"),
748 "Token file should NOT contain plaintext gateway_code: {}",
749 raw_string
750 );
751 assert!(
752 !raw_string.contains("harmony:admin"),
753 "Token file should NOT contain plaintext abilities: {}",
754 raw_string
755 );
756
757 if raw_contents.len() > 50 {
759 let has_age_header = raw_string.starts_with("age-encryption.org/v1");
761 assert!(
762 has_age_header || raw_contents.starts_with(b"age-encryption.org/v1"),
763 "File should contain age encryption header. Raw contents (first 100 bytes): {:?}",
764 &raw_contents[..std::cmp::min(100, raw_contents.len())]
765 );
766 }
767
768 let decrypted_data = storage.read_file_str("auth.json").await.unwrap();
770 let loaded_token: MachineToken = serde_json::from_slice(&decrypted_data).unwrap();
771 assert_eq!(loaded_token.machine_token, "super_secret_token_12345");
772 assert_eq!(loaded_token.gateway_id, "gw_secret");
773 }
774
775 #[tokio::test]
776 async fn test_token_file_cannot_be_read_as_json() {
777 use crate::storage::EncryptedFilesystemStorage;
778 use tempfile::TempDir;
779
780 let instance_id = "test-raw-json-read";
781 let temp_dir = TempDir::new().unwrap();
782 let storage_path = temp_dir.path().join(instance_id);
783
784 let token = MachineToken::new(
785 "test_token_json".to_string(),
786 "2099-12-31T23:59:59Z".to_string(),
787 "gw_json".to_string(),
788 "json-test".to_string(),
789 vec![],
790 );
791
792 let storage = EncryptedFilesystemStorage::new(&storage_path)
794 .await
795 .unwrap();
796 let token_json = serde_json::to_vec(&token).unwrap();
797 storage
798 .write_file_str("auth.json", &token_json)
799 .await
800 .unwrap();
801
802 let token_path = storage_path.join("auth.json");
804
805 let raw_contents = std::fs::read(&token_path).unwrap();
807
808 let json_parse_result: Result<serde_json::Value, _> = serde_json::from_slice(&raw_contents);
810 assert!(
811 json_parse_result.is_err(),
812 "Raw token file should NOT be parseable as JSON (it should be encrypted)"
813 );
814 }
815
816 #[tokio::test]
817 async fn test_token_different_from_plaintext() {
818 use crate::storage::EncryptedFilesystemStorage;
819 use tempfile::TempDir;
820
821 let instance_id = "test-plaintext-compare";
822 let temp_dir = TempDir::new().unwrap();
823 let storage_path = temp_dir.path().join(instance_id);
824
825 let token = MachineToken::new(
826 "comparison_token".to_string(),
827 "2099-12-31T23:59:59Z".to_string(),
828 "gw_compare".to_string(),
829 "compare-gateway".to_string(),
830 vec!["test:ability".to_string()],
831 );
832
833 let plaintext_json = serde_json::to_vec(&token).unwrap();
835
836 let storage = EncryptedFilesystemStorage::new(&storage_path)
838 .await
839 .unwrap();
840 storage
841 .write_file_str("auth.json", &plaintext_json)
842 .await
843 .unwrap();
844
845 let token_path = storage_path.join("auth.json");
847 let encrypted_contents = std::fs::read(&token_path).unwrap();
848
849 assert_ne!(
851 encrypted_contents, plaintext_json,
852 "Encrypted file contents should differ from plaintext JSON"
853 );
854
855 assert!(
857 encrypted_contents.len() > plaintext_json.len(),
858 "Encrypted file should be larger due to encryption overhead. Encrypted: {}, Plaintext: {}",
859 encrypted_contents.len(),
860 plaintext_json.len()
861 );
862 }
863
864 #[tokio::test]
865 async fn test_multiple_instances_isolated() {
866 use crate::storage::EncryptedFilesystemStorage;
867 use tempfile::TempDir;
868
869 let temp_dir = TempDir::new().unwrap();
870
871 let token1 = MachineToken::new(
873 "token_instance_1".to_string(),
874 "2099-12-31T23:59:59Z".to_string(),
875 "gw_1".to_string(),
876 "gateway-1".to_string(),
877 vec![],
878 );
879
880 let token2 = MachineToken::new(
881 "token_instance_2".to_string(),
882 "2099-12-31T23:59:59Z".to_string(),
883 "gw_2".to_string(),
884 "gateway-2".to_string(),
885 vec![],
886 );
887
888 let storage1_path = temp_dir.path().join("instance-1");
890 let storage2_path = temp_dir.path().join("instance-2");
891
892 let storage1 = EncryptedFilesystemStorage::new(&storage1_path)
893 .await
894 .unwrap();
895 let storage2 = EncryptedFilesystemStorage::new(&storage2_path)
896 .await
897 .unwrap();
898
899 let token1_json = serde_json::to_vec(&token1).unwrap();
901 let token2_json = serde_json::to_vec(&token2).unwrap();
902 storage1
903 .write_file_str("auth.json", &token1_json)
904 .await
905 .unwrap();
906 storage2
907 .write_file_str("auth.json", &token2_json)
908 .await
909 .unwrap();
910
911 let path1 = storage1_path.join("auth.json");
913 let path2 = storage2_path.join("auth.json");
914
915 assert!(path1.exists(), "Instance 1 token file should exist");
916 assert!(path2.exists(), "Instance 2 token file should exist");
917 assert_ne!(path1, path2, "Token files should be in different locations");
918
919 let key1_path = storage1_path.join("encryption.key");
921 let key2_path = storage2_path.join("encryption.key");
922
923 if key1_path.exists() && key2_path.exists() {
924 let key1_contents = std::fs::read(&key1_path).unwrap();
925 let key2_contents = std::fs::read(&key2_path).unwrap();
926 assert_ne!(
927 key1_contents, key2_contents,
928 "Encryption keys should be different for each instance"
929 );
930 }
931
932 let decrypted1 = storage1.read_file_str("auth.json").await.unwrap();
934 let decrypted2 = storage2.read_file_str("auth.json").await.unwrap();
935
936 let loaded1: MachineToken = serde_json::from_slice(&decrypted1).unwrap();
937 let loaded2: MachineToken = serde_json::from_slice(&decrypted2).unwrap();
938
939 assert_eq!(loaded1.machine_token, "token_instance_1");
940 assert_eq!(loaded1.gateway_code, "gateway-1");
941 assert_eq!(loaded2.machine_token, "token_instance_2");
942 assert_eq!(loaded2.gateway_code, "gateway-2");
943 }
944
945 #[tokio::test]
946 #[cfg(unix)]
947 async fn test_encryption_key_file_permissions() {
948 use crate::storage::EncryptedFilesystemStorage;
949 use std::os::unix::fs::PermissionsExt;
950 use tempfile::TempDir;
951
952 let instance_id = "test-key-permissions";
953 let temp_dir = TempDir::new().unwrap();
954 let storage_path = temp_dir.path().join(instance_id);
955
956 let _storage = EncryptedFilesystemStorage::new(&storage_path)
958 .await
959 .unwrap();
960
961 let key_path = storage_path.join("encryption.key");
963
964 if !key_path.exists() {
966 return;
968 }
969
970 let metadata = std::fs::metadata(&key_path).unwrap();
971 let permissions = metadata.permissions();
972 let mode = permissions.mode();
973
974 let permission_bits = mode & 0o777;
976 assert_eq!(
977 permission_bits, 0o600,
978 "Encryption key file should have 0600 permissions (owner read/write only), got {:o}",
979 permission_bits
980 );
981 }
982
983 #[tokio::test]
984 async fn test_tampered_token_file_fails_to_load() {
985 use crate::storage::EncryptedFilesystemStorage;
986 use tempfile::TempDir;
987
988 let instance_id = "test-tamper";
989 let temp_dir = TempDir::new().unwrap();
990 let storage_path = temp_dir.path().join(instance_id);
991
992 let token = MachineToken::new(
993 "original_token".to_string(),
994 "2099-12-31T23:59:59Z".to_string(),
995 "gw_tamper".to_string(),
996 "tamper-test".to_string(),
997 vec![],
998 );
999
1000 let storage = EncryptedFilesystemStorage::new(&storage_path)
1002 .await
1003 .unwrap();
1004 let token_json = serde_json::to_vec(&token).unwrap();
1005 storage
1006 .write_file_str("auth.json", &token_json)
1007 .await
1008 .unwrap();
1009
1010 let token_path = storage_path.join("auth.json");
1012 let mut contents = std::fs::read(&token_path).unwrap();
1013
1014 if contents.len() > 50 {
1016 contents[25] = contents[25].wrapping_add(1);
1017 contents[30] = contents[30].wrapping_sub(1);
1018 std::fs::write(&token_path, contents).unwrap();
1019 }
1020
1021 let result = storage.read_file_str("auth.json").await;
1023 assert!(
1024 result.is_err(),
1025 "Loading tampered encrypted file should fail"
1026 );
1027 }
1028
1029 #[tokio::test]
1034 #[serial]
1035 async fn test_generic_save_and_load_user_token() {
1036 use crate::runbeam_api::types::UserToken;
1037 let _guard = setup_test_encryption();
1038 let instance_id = "test-user-token";
1039 clear_token(instance_id, "user_auth").await.ok();
1040
1041 let user_token = UserToken::new(
1042 "user_jwt_token".to_string(),
1043 Some(1234567890),
1044 Some(crate::runbeam_api::types::UserInfo {
1045 id: "user123".to_string(),
1046 name: "Test User".to_string(),
1047 email: "test@example.com".to_string(),
1048 }),
1049 );
1050
1051 save_token(instance_id, "user_auth", &user_token)
1053 .await
1054 .unwrap();
1055
1056 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1058 assert!(loaded.is_some());
1059
1060 let loaded_token = loaded.unwrap();
1061 assert_eq!(loaded_token.token, user_token.token);
1062 assert_eq!(loaded_token.expires_at, user_token.expires_at);
1063 assert!(loaded_token.user.is_some());
1064
1065 clear_token(instance_id, "user_auth").await.unwrap();
1067 }
1068
1069 #[tokio::test]
1070 #[serial]
1071 async fn test_different_token_types_isolated() {
1072 use crate::runbeam_api::types::UserToken;
1073 let _guard = setup_test_encryption();
1074 let instance_id = "test-isolation";
1075
1076 let user_token = UserToken::new("user_token".to_string(), None, None);
1078
1079 let machine_token = MachineToken::new(
1080 "machine_token".to_string(),
1081 "2099-12-31T23:59:59Z".to_string(),
1082 "gw_test".to_string(),
1083 "test-gw".to_string(),
1084 vec![],
1085 );
1086
1087 save_token(instance_id, "user_auth", &user_token)
1089 .await
1090 .unwrap();
1091 save_token(instance_id, "auth", &machine_token)
1092 .await
1093 .unwrap();
1094
1095 let loaded_user: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1097 let loaded_machine: Option<MachineToken> = load_token(instance_id, "auth").await.unwrap();
1098
1099 assert!(loaded_user.is_some());
1100 assert!(loaded_machine.is_some());
1101 assert_eq!(loaded_user.unwrap().token, "user_token");
1102 assert_eq!(loaded_machine.unwrap().machine_token, "machine_token");
1103
1104 clear_token(instance_id, "user_auth").await.unwrap();
1106 clear_token(instance_id, "auth").await.unwrap();
1107 }
1108
1109 #[tokio::test]
1110 #[serial]
1111 async fn test_user_token_with_full_metadata() {
1112 use crate::runbeam_api::types::UserToken;
1113 let _guard = setup_test_encryption();
1114 let instance_id = "test-user-full";
1115 clear_token(instance_id, "user_auth").await.ok();
1116
1117 let user_token = UserToken::new(
1118 "detailed_user_token".to_string(),
1119 Some(2000000000),
1120 Some(crate::runbeam_api::types::UserInfo {
1121 id: "user456".to_string(),
1122 name: "John Doe".to_string(),
1123 email: "john@example.com".to_string(),
1124 }),
1125 );
1126
1127 save_token(instance_id, "user_auth", &user_token)
1129 .await
1130 .unwrap();
1131 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1132
1133 assert!(loaded.is_some());
1134 let loaded_token = loaded.unwrap();
1135 assert_eq!(loaded_token.token, "detailed_user_token");
1136 assert_eq!(loaded_token.expires_at, Some(2000000000));
1137
1138 let user_info = loaded_token.user.unwrap();
1139 assert_eq!(user_info.id, "user456");
1140 assert_eq!(user_info.name, "John Doe");
1141 assert_eq!(user_info.email, "john@example.com");
1142
1143 clear_token(instance_id, "user_auth").await.unwrap();
1145 }
1146}