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)
335 .await?;
336
337 let json = serde_json::to_vec_pretty(&token).map_err(|e| {
339 tracing::error!("Failed to serialize machine token: {}", e);
340 StorageError::Config(format!("JSON serialization failed: {}", e))
341 })?;
342
343 storage.write_file_str(token_path, &json).await?;
345
346 tracing::info!(
347 "Machine token saved successfully with explicit key: gateway_id={}, expires_at={}",
348 token.gateway_id,
349 token.expires_at
350 );
351
352 Ok(())
353}
354
355pub async fn save_machine_token(
377 instance_id: &str,
378 token: &MachineToken,
379) -> Result<(), StorageError> {
380 save_token(instance_id, "auth", token).await
381}
382
383pub async fn load_machine_token(instance_id: &str) -> Result<Option<MachineToken>, StorageError> {
399 load_token(instance_id, "auth").await
400}
401
402pub async fn clear_machine_token(instance_id: &str) -> Result<(), StorageError> {
417 clear_token(instance_id, "auth").await
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use serial_test::serial;
424
425 fn setup_test_encryption() -> impl Drop {
427 use base64::Engine;
428 use secrecy::ExposeSecret;
429 use std::env;
430
431 let identity = age::x25519::Identity::generate();
432 let key_base64 = base64::engine::general_purpose::STANDARD
433 .encode(identity.to_string().expose_secret().as_bytes());
434 env::set_var("RUNBEAM_ENCRYPTION_KEY", &key_base64);
435
436
437 struct Guard;
439 impl Drop for Guard {
440 fn drop(&mut self) {
441 std::env::remove_var("RUNBEAM_ENCRYPTION_KEY");
442 }
443 }
444 Guard
445 }
446
447 #[test]
448 fn test_machine_token_creation() {
449 let token = MachineToken::new(
450 "test_token".to_string(),
451 "2025-12-31T23:59:59Z".to_string(),
452 "gw123".to_string(),
453 "gateway-code-123".to_string(),
454 vec!["harmony:send".to_string(), "harmony:receive".to_string()],
455 );
456
457 assert_eq!(token.machine_token, "test_token");
458 assert_eq!(token.gateway_id, "gw123");
459 assert_eq!(token.gateway_code, "gateway-code-123");
460 assert_eq!(token.abilities.len(), 2);
461 assert!(!token.issued_at.is_empty());
462 }
463
464 #[test]
465 fn test_machine_token_is_expired() {
466 let expired_token = MachineToken::new(
468 "test_token".to_string(),
469 "2020-01-01T00:00:00Z".to_string(),
470 "gw123".to_string(),
471 "gateway-code-123".to_string(),
472 vec![],
473 );
474 assert!(expired_token.is_expired());
475 assert!(!expired_token.is_valid());
476
477 let valid_token = MachineToken::new(
479 "test_token".to_string(),
480 "2099-12-31T23:59:59Z".to_string(),
481 "gw123".to_string(),
482 "gateway-code-123".to_string(),
483 vec![],
484 );
485 assert!(!valid_token.is_expired());
486 assert!(valid_token.is_valid());
487 }
488
489 #[test]
490 fn test_machine_token_serialization() {
491 let token = MachineToken::new(
492 "test_token".to_string(),
493 "2025-12-31T23:59:59Z".to_string(),
494 "gw123".to_string(),
495 "gateway-code-123".to_string(),
496 vec!["harmony:send".to_string()],
497 );
498
499 let json = serde_json::to_string(&token).unwrap();
500 assert!(json.contains("\"machine_token\":\"test_token\""));
501 assert!(json.contains("\"gateway_id\":\"gw123\""));
502 assert!(json.contains("\"gateway_code\":\"gateway-code-123\""));
503
504 let deserialized: MachineToken = serde_json::from_str(&json).unwrap();
506 assert_eq!(deserialized.machine_token, token.machine_token);
507 assert_eq!(deserialized.gateway_id, token.gateway_id);
508 }
509
510 #[tokio::test]
511 #[serial]
512 async fn test_save_and_load_token_secure() {
513 let _guard = setup_test_encryption();
514 let instance_id = "test-save-load";
515 let _ = clear_machine_token(instance_id).await;
517
518 let token = MachineToken::new(
519 "test_token_secure".to_string(),
520 "2099-12-31T23:59:59Z".to_string(),
521 "gw_test".to_string(),
522 "test-gateway".to_string(),
523 vec!["harmony:send".to_string()],
524 );
525
526 save_machine_token(instance_id, &token).await.unwrap();
528
529 let loaded = load_machine_token(instance_id).await.unwrap();
531 assert!(loaded.is_some());
532
533 let loaded_token = loaded.unwrap();
534 assert_eq!(loaded_token.machine_token, token.machine_token);
535 assert_eq!(loaded_token.gateway_id, token.gateway_id);
536 assert_eq!(loaded_token.gateway_code, token.gateway_code);
537 assert!(loaded_token.is_valid());
538
539 clear_machine_token(instance_id).await.unwrap();
541 }
542
543 #[tokio::test]
544 #[serial]
545 async fn test_load_nonexistent_token_secure() {
546 let _guard = setup_test_encryption();
547 let instance_id = "test-nonexistent";
548 let _ = clear_machine_token(instance_id).await;
550
551 let result = load_machine_token(instance_id).await.unwrap();
553 assert!(result.is_none());
554 }
555
556 #[tokio::test]
557 #[serial]
558 async fn test_clear_token_secure() {
559 let _guard = setup_test_encryption();
560 let instance_id = "test-clear";
561 let _ = clear_machine_token(instance_id).await;
563
564 let token = MachineToken::new(
565 "test_clear".to_string(),
566 "2099-12-31T23:59:59Z".to_string(),
567 "gw_clear".to_string(),
568 "clear-test".to_string(),
569 vec![],
570 );
571
572 save_machine_token(instance_id, &token).await.unwrap();
574
575 assert!(load_machine_token(instance_id).await.unwrap().is_some());
577
578 clear_machine_token(instance_id).await.unwrap();
580
581 assert!(load_machine_token(instance_id).await.unwrap().is_none());
583 }
584
585 #[tokio::test]
586 #[serial]
587 async fn test_clear_nonexistent_token_secure() {
588 let _guard = setup_test_encryption();
589 let instance_id = "test-clear-nonexistent";
590 clear_machine_token(instance_id).await.unwrap();
592 }
593
594 #[tokio::test]
595 #[serial]
596 async fn test_token_expiry_detection() {
597 let _guard = setup_test_encryption();
598 let instance_id = "test-expiry";
599 let _ = clear_machine_token(instance_id).await;
600
601 let expired_token = MachineToken::new(
603 "expired_token".to_string(),
604 "2020-01-01T00:00:00Z".to_string(),
605 "gw_expired".to_string(),
606 "expired-gateway".to_string(),
607 vec![],
608 );
609
610 save_machine_token(instance_id, &expired_token)
611 .await
612 .unwrap();
613
614 let loaded = load_machine_token(instance_id).await.unwrap();
616 assert!(loaded.is_some());
617 let loaded_token = loaded.unwrap();
618 assert!(loaded_token.is_expired());
619 assert!(!loaded_token.is_valid());
620
621 clear_machine_token(instance_id).await.unwrap();
623 }
624
625 #[tokio::test]
626 #[serial]
627 async fn test_token_with_abilities() {
628 let _guard = setup_test_encryption();
629 let instance_id = "test-abilities";
630 let _ = clear_machine_token(instance_id).await;
631
632 let token = MachineToken::new(
633 "token_with_abilities".to_string(),
634 "2099-12-31T23:59:59Z".to_string(),
635 "gw_abilities".to_string(),
636 "abilities-test".to_string(),
637 vec![
638 "harmony:send".to_string(),
639 "harmony:receive".to_string(),
640 "harmony:config".to_string(),
641 ],
642 );
643
644 save_machine_token(instance_id, &token).await.unwrap();
645
646 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
647 assert_eq!(loaded.abilities.len(), 3);
648 assert!(loaded.abilities.contains(&"harmony:send".to_string()));
649 assert!(loaded.abilities.contains(&"harmony:receive".to_string()));
650 assert!(loaded.abilities.contains(&"harmony:config".to_string()));
651
652 clear_machine_token(instance_id).await.unwrap();
654 }
655
656 #[tokio::test]
657 #[serial]
658 async fn test_token_overwrites_existing() {
659 let _guard = setup_test_encryption();
660 let instance_id = "test-overwrite";
661 let _ = clear_machine_token(instance_id).await;
662
663 let token1 = MachineToken::new(
665 "first_token".to_string(),
666 "2099-12-31T23:59:59Z".to_string(),
667 "gw_first".to_string(),
668 "first-gateway".to_string(),
669 vec![],
670 );
671 save_machine_token(instance_id, &token1).await.unwrap();
672
673 let token2 = MachineToken::new(
675 "second_token".to_string(),
676 "2099-12-31T23:59:59Z".to_string(),
677 "gw_second".to_string(),
678 "second-gateway".to_string(),
679 vec![],
680 );
681 save_machine_token(instance_id, &token2).await.unwrap();
682
683 let loaded = load_machine_token(instance_id).await.unwrap().unwrap();
685 assert_eq!(loaded.machine_token, "second_token");
686 assert_eq!(loaded.gateway_id, "gw_second");
687 assert_eq!(loaded.gateway_code, "second-gateway");
688
689 clear_machine_token(instance_id).await.unwrap();
691 }
692
693 #[tokio::test]
694 async fn test_token_encrypted_on_disk() {
695 use crate::storage::EncryptedFilesystemStorage;
696 use tempfile::TempDir;
697
698 let instance_id = "test-encryption-verify";
699 let temp_dir = TempDir::new().unwrap();
700
701 let token = MachineToken::new(
703 "super_secret_token_12345".to_string(),
704 "2099-12-31T23:59:59Z".to_string(),
705 "gw_secret".to_string(),
706 "secret-gateway".to_string(),
707 vec!["harmony:admin".to_string()],
708 );
709
710 let storage_path = temp_dir.path().join(instance_id);
712 let storage = EncryptedFilesystemStorage::new(&storage_path)
713 .await
714 .unwrap();
715
716 let token_json = serde_json::to_vec(&token).unwrap();
718 storage
719 .write_file_str("auth.json", &token_json)
720 .await
721 .unwrap();
722
723 let token_path = storage_path.join("auth.json");
725
726 assert!(
728 token_path.exists(),
729 "Token file should exist at {:?}",
730 token_path
731 );
732
733 let raw_contents = std::fs::read(&token_path).unwrap();
735 let raw_string = String::from_utf8_lossy(&raw_contents);
736
737 assert!(
739 !raw_string.contains("super_secret_token_12345"),
740 "Token file should NOT contain plaintext token: {}",
741 raw_string
742 );
743 assert!(
744 !raw_string.contains("gw_secret"),
745 "Token file should NOT contain plaintext gateway_id: {}",
746 raw_string
747 );
748 assert!(
749 !raw_string.contains("secret-gateway"),
750 "Token file should NOT contain plaintext gateway_code: {}",
751 raw_string
752 );
753 assert!(
754 !raw_string.contains("harmony:admin"),
755 "Token file should NOT contain plaintext abilities: {}",
756 raw_string
757 );
758
759 if raw_contents.len() > 50 {
761 let has_age_header = raw_string.starts_with("age-encryption.org/v1");
763 assert!(
764 has_age_header || raw_contents.starts_with(b"age-encryption.org/v1"),
765 "File should contain age encryption header. Raw contents (first 100 bytes): {:?}",
766 &raw_contents[..std::cmp::min(100, raw_contents.len())]
767 );
768 }
769
770 let decrypted_data = storage.read_file_str("auth.json").await.unwrap();
772 let loaded_token: MachineToken = serde_json::from_slice(&decrypted_data).unwrap();
773 assert_eq!(loaded_token.machine_token, "super_secret_token_12345");
774 assert_eq!(loaded_token.gateway_id, "gw_secret");
775 }
776
777 #[tokio::test]
778 async fn test_token_file_cannot_be_read_as_json() {
779 use crate::storage::EncryptedFilesystemStorage;
780 use tempfile::TempDir;
781
782 let instance_id = "test-raw-json-read";
783 let temp_dir = TempDir::new().unwrap();
784 let storage_path = temp_dir.path().join(instance_id);
785
786 let token = MachineToken::new(
787 "test_token_json".to_string(),
788 "2099-12-31T23:59:59Z".to_string(),
789 "gw_json".to_string(),
790 "json-test".to_string(),
791 vec![],
792 );
793
794 let storage = EncryptedFilesystemStorage::new(&storage_path)
796 .await
797 .unwrap();
798 let token_json = serde_json::to_vec(&token).unwrap();
799 storage
800 .write_file_str("auth.json", &token_json)
801 .await
802 .unwrap();
803
804 let token_path = storage_path.join("auth.json");
806
807 let raw_contents = std::fs::read(&token_path).unwrap();
809
810 let json_parse_result: Result<serde_json::Value, _> = serde_json::from_slice(&raw_contents);
812 assert!(
813 json_parse_result.is_err(),
814 "Raw token file should NOT be parseable as JSON (it should be encrypted)"
815 );
816 }
817
818 #[tokio::test]
819 async fn test_token_different_from_plaintext() {
820 use crate::storage::EncryptedFilesystemStorage;
821 use tempfile::TempDir;
822
823 let instance_id = "test-plaintext-compare";
824 let temp_dir = TempDir::new().unwrap();
825 let storage_path = temp_dir.path().join(instance_id);
826
827 let token = MachineToken::new(
828 "comparison_token".to_string(),
829 "2099-12-31T23:59:59Z".to_string(),
830 "gw_compare".to_string(),
831 "compare-gateway".to_string(),
832 vec!["test:ability".to_string()],
833 );
834
835 let plaintext_json = serde_json::to_vec(&token).unwrap();
837
838 let storage = EncryptedFilesystemStorage::new(&storage_path)
840 .await
841 .unwrap();
842 storage
843 .write_file_str("auth.json", &plaintext_json)
844 .await
845 .unwrap();
846
847 let token_path = storage_path.join("auth.json");
849 let encrypted_contents = std::fs::read(&token_path).unwrap();
850
851 assert_ne!(
853 encrypted_contents, plaintext_json,
854 "Encrypted file contents should differ from plaintext JSON"
855 );
856
857 assert!(
859 encrypted_contents.len() > plaintext_json.len(),
860 "Encrypted file should be larger due to encryption overhead. Encrypted: {}, Plaintext: {}",
861 encrypted_contents.len(),
862 plaintext_json.len()
863 );
864 }
865
866 #[tokio::test]
867 async fn test_multiple_instances_isolated() {
868 use crate::storage::EncryptedFilesystemStorage;
869 use tempfile::TempDir;
870
871 let temp_dir = TempDir::new().unwrap();
872
873 let token1 = MachineToken::new(
875 "token_instance_1".to_string(),
876 "2099-12-31T23:59:59Z".to_string(),
877 "gw_1".to_string(),
878 "gateway-1".to_string(),
879 vec![],
880 );
881
882 let token2 = MachineToken::new(
883 "token_instance_2".to_string(),
884 "2099-12-31T23:59:59Z".to_string(),
885 "gw_2".to_string(),
886 "gateway-2".to_string(),
887 vec![],
888 );
889
890 let storage1_path = temp_dir.path().join("instance-1");
892 let storage2_path = temp_dir.path().join("instance-2");
893
894 let storage1 = EncryptedFilesystemStorage::new(&storage1_path)
895 .await
896 .unwrap();
897 let storage2 = EncryptedFilesystemStorage::new(&storage2_path)
898 .await
899 .unwrap();
900
901 let token1_json = serde_json::to_vec(&token1).unwrap();
903 let token2_json = serde_json::to_vec(&token2).unwrap();
904 storage1
905 .write_file_str("auth.json", &token1_json)
906 .await
907 .unwrap();
908 storage2
909 .write_file_str("auth.json", &token2_json)
910 .await
911 .unwrap();
912
913 let path1 = storage1_path.join("auth.json");
915 let path2 = storage2_path.join("auth.json");
916
917 assert!(path1.exists(), "Instance 1 token file should exist");
918 assert!(path2.exists(), "Instance 2 token file should exist");
919 assert_ne!(path1, path2, "Token files should be in different locations");
920
921 let key1_path = storage1_path.join("encryption.key");
923 let key2_path = storage2_path.join("encryption.key");
924
925 if key1_path.exists() && key2_path.exists() {
926 let key1_contents = std::fs::read(&key1_path).unwrap();
927 let key2_contents = std::fs::read(&key2_path).unwrap();
928 assert_ne!(
929 key1_contents, key2_contents,
930 "Encryption keys should be different for each instance"
931 );
932 }
933
934 let decrypted1 = storage1.read_file_str("auth.json").await.unwrap();
936 let decrypted2 = storage2.read_file_str("auth.json").await.unwrap();
937
938 let loaded1: MachineToken = serde_json::from_slice(&decrypted1).unwrap();
939 let loaded2: MachineToken = serde_json::from_slice(&decrypted2).unwrap();
940
941 assert_eq!(loaded1.machine_token, "token_instance_1");
942 assert_eq!(loaded1.gateway_code, "gateway-1");
943 assert_eq!(loaded2.machine_token, "token_instance_2");
944 assert_eq!(loaded2.gateway_code, "gateway-2");
945 }
946
947 #[tokio::test]
948 #[cfg(unix)]
949 async fn test_encryption_key_file_permissions() {
950 use crate::storage::EncryptedFilesystemStorage;
951 use std::os::unix::fs::PermissionsExt;
952 use tempfile::TempDir;
953
954 let instance_id = "test-key-permissions";
955 let temp_dir = TempDir::new().unwrap();
956 let storage_path = temp_dir.path().join(instance_id);
957
958 let _storage = EncryptedFilesystemStorage::new(&storage_path)
960 .await
961 .unwrap();
962
963 let key_path = storage_path.join("encryption.key");
965
966 if !key_path.exists() {
968 return;
970 }
971
972 let metadata = std::fs::metadata(&key_path).unwrap();
973 let permissions = metadata.permissions();
974 let mode = permissions.mode();
975
976 let permission_bits = mode & 0o777;
978 assert_eq!(
979 permission_bits, 0o600,
980 "Encryption key file should have 0600 permissions (owner read/write only), got {:o}",
981 permission_bits
982 );
983 }
984
985 #[tokio::test]
986 async fn test_tampered_token_file_fails_to_load() {
987 use crate::storage::EncryptedFilesystemStorage;
988 use tempfile::TempDir;
989
990 let instance_id = "test-tamper";
991 let temp_dir = TempDir::new().unwrap();
992 let storage_path = temp_dir.path().join(instance_id);
993
994 let token = MachineToken::new(
995 "original_token".to_string(),
996 "2099-12-31T23:59:59Z".to_string(),
997 "gw_tamper".to_string(),
998 "tamper-test".to_string(),
999 vec![],
1000 );
1001
1002 let storage = EncryptedFilesystemStorage::new(&storage_path)
1004 .await
1005 .unwrap();
1006 let token_json = serde_json::to_vec(&token).unwrap();
1007 storage
1008 .write_file_str("auth.json", &token_json)
1009 .await
1010 .unwrap();
1011
1012 let token_path = storage_path.join("auth.json");
1014 let mut contents = std::fs::read(&token_path).unwrap();
1015
1016 if contents.len() > 50 {
1018 contents[25] = contents[25].wrapping_add(1);
1019 contents[30] = contents[30].wrapping_sub(1);
1020 std::fs::write(&token_path, contents).unwrap();
1021 }
1022
1023 let result = storage.read_file_str("auth.json").await;
1025 assert!(
1026 result.is_err(),
1027 "Loading tampered encrypted file should fail"
1028 );
1029 }
1030
1031 #[tokio::test]
1036 #[serial]
1037 async fn test_generic_save_and_load_user_token() {
1038 use crate::runbeam_api::types::UserToken;
1039 let _guard = setup_test_encryption();
1040 let instance_id = "test-user-token";
1041 clear_token(instance_id, "user_auth").await.ok();
1042
1043 let user_token = UserToken::new(
1044 "user_jwt_token".to_string(),
1045 Some(1234567890),
1046 Some(crate::runbeam_api::types::UserInfo {
1047 id: "user123".to_string(),
1048 name: "Test User".to_string(),
1049 email: "test@example.com".to_string(),
1050 }),
1051 );
1052
1053 save_token(instance_id, "user_auth", &user_token)
1055 .await
1056 .unwrap();
1057
1058 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1060 assert!(loaded.is_some());
1061
1062 let loaded_token = loaded.unwrap();
1063 assert_eq!(loaded_token.token, user_token.token);
1064 assert_eq!(loaded_token.expires_at, user_token.expires_at);
1065 assert!(loaded_token.user.is_some());
1066
1067 clear_token(instance_id, "user_auth").await.unwrap();
1069 }
1070
1071 #[tokio::test]
1072 #[serial]
1073 async fn test_different_token_types_isolated() {
1074 use crate::runbeam_api::types::UserToken;
1075 let _guard = setup_test_encryption();
1076 let instance_id = "test-isolation";
1077
1078 let user_token = UserToken::new("user_token".to_string(), None, None);
1080
1081 let machine_token = MachineToken::new(
1082 "machine_token".to_string(),
1083 "2099-12-31T23:59:59Z".to_string(),
1084 "gw_test".to_string(),
1085 "test-gw".to_string(),
1086 vec![],
1087 );
1088
1089 save_token(instance_id, "user_auth", &user_token)
1091 .await
1092 .unwrap();
1093 save_token(instance_id, "auth", &machine_token)
1094 .await
1095 .unwrap();
1096
1097 let loaded_user: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1099 let loaded_machine: Option<MachineToken> = load_token(instance_id, "auth").await.unwrap();
1100
1101 assert!(loaded_user.is_some());
1102 assert!(loaded_machine.is_some());
1103 assert_eq!(loaded_user.unwrap().token, "user_token");
1104 assert_eq!(loaded_machine.unwrap().machine_token, "machine_token");
1105
1106 clear_token(instance_id, "user_auth").await.unwrap();
1108 clear_token(instance_id, "auth").await.unwrap();
1109 }
1110
1111 #[tokio::test]
1112 #[serial]
1113 async fn test_user_token_with_full_metadata() {
1114 use crate::runbeam_api::types::UserToken;
1115 let _guard = setup_test_encryption();
1116 let instance_id = "test-user-full";
1117 clear_token(instance_id, "user_auth").await.ok();
1118
1119 let user_token = UserToken::new(
1120 "detailed_user_token".to_string(),
1121 Some(2000000000),
1122 Some(crate::runbeam_api::types::UserInfo {
1123 id: "user456".to_string(),
1124 name: "John Doe".to_string(),
1125 email: "john@example.com".to_string(),
1126 }),
1127 );
1128
1129 save_token(instance_id, "user_auth", &user_token)
1131 .await
1132 .unwrap();
1133 let loaded: Option<UserToken> = load_token(instance_id, "user_auth").await.unwrap();
1134
1135 assert!(loaded.is_some());
1136 let loaded_token = loaded.unwrap();
1137 assert_eq!(loaded_token.token, "detailed_user_token");
1138 assert_eq!(loaded_token.expires_at, Some(2000000000));
1139
1140 let user_info = loaded_token.user.unwrap();
1141 assert_eq!(user_info.id, "user456");
1142 assert_eq!(user_info.name, "John Doe");
1143 assert_eq!(user_info.email, "john@example.com");
1144
1145 clear_token(instance_id, "user_auth").await.unwrap();
1147 }
1148}