1use crate::{AppPaths, MigrationError, Migrator};
42use std::path::Path;
43
44pub use local_store::{AtomicWriteConfig, DirStorageStrategy, FilenameEncoding, FormatStrategy};
46
47pub struct DirStorage {
59 inner: local_store::DirStorage,
61 migrator: Migrator,
63 strategy: local_store::DirStorageStrategy,
65}
66
67impl DirStorage {
68 pub fn new(
87 paths: AppPaths,
88 domain_name: &str,
89 migrator: Migrator,
90 strategy: DirStorageStrategy,
91 ) -> Result<Self, MigrationError> {
92 let inner = local_store::DirStorage::new(paths, domain_name, strategy.clone())
93 .map_err(store_err_to_migration)?;
94 Ok(Self {
95 inner,
96 migrator,
97 strategy,
98 })
99 }
100
101 pub fn save<T>(&self, entity_name: &str, id: &str, entity: T) -> Result<(), MigrationError>
123 where
124 T: serde::Serialize,
125 {
126 let json_string = self.migrator.save_domain_flat(entity_name, entity)?;
127
128 let versioned_value: serde_json::Value = serde_json::from_str(&json_string)
129 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
130
131 let content = match self.strategy.format {
132 FormatStrategy::Json => serde_json::to_string_pretty(&versioned_value)
133 .map_err(|e| MigrationError::SerializationError(e.to_string()))?,
134 FormatStrategy::Toml => {
135 let tv = local_store::json_to_toml(&versioned_value).map_err(|e| {
136 MigrationError::Store(local_store::StoreError::FormatConvert(e))
137 })?;
138 toml::to_string_pretty(&tv)
139 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))?
140 }
141 };
142
143 self.inner
144 .save_raw_string(entity_name, id, &content)
145 .map_err(store_err_to_migration)
146 }
147
148 pub fn load<D>(&self, entity_name: &str, id: &str) -> Result<D, MigrationError>
166 where
167 D: serde::de::DeserializeOwned,
168 {
169 let content = self
170 .inner
171 .load_raw_string(id)
172 .map_err(store_err_to_migration)?;
173
174 let value = match self.strategy.format {
175 FormatStrategy::Json => serde_json::from_str(&content)
176 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?,
177 FormatStrategy::Toml => {
178 let tv: toml::Value = toml::from_str(&content)
179 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
180 toml_to_json(tv)?
181 }
182 };
183
184 self.migrator.load_flat_from(entity_name, value)
185 }
186
187 pub fn list_ids(&self) -> Result<Vec<String>, MigrationError> {
198 let mut ids = self.inner.list_ids().map_err(store_err_to_migration)?;
199 ids.sort();
201 Ok(ids)
202 }
203
204 pub fn load_all<D>(&self, entity_name: &str) -> Result<Vec<(String, D)>, MigrationError>
219 where
220 D: serde::de::DeserializeOwned,
221 {
222 let ids = self.list_ids()?;
223 let mut results = Vec::new();
224 for id in ids {
225 let entity = self.load(entity_name, &id)?;
226 results.push((id, entity));
227 }
228 Ok(results)
229 }
230
231 pub fn exists(&self, id: &str) -> Result<bool, MigrationError> {
245 self.inner.exists(id).map_err(store_err_to_migration)
246 }
247
248 pub fn delete(&self, id: &str) -> Result<(), MigrationError> {
263 self.inner.delete(id).map_err(store_err_to_migration)
264 }
265
266 pub fn base_path(&self) -> &Path {
272 self.inner.base_path()
273 }
274}
275
276fn store_err_to_migration(e: local_store::StoreError) -> MigrationError {
280 match e {
281 local_store::StoreError::FilenameEncoding { id, reason } => {
282 MigrationError::FilenameEncoding { id, reason }
283 }
284 other => MigrationError::Store(other),
285 }
286}
287
288fn toml_to_json(toml_value: toml::Value) -> Result<serde_json::Value, MigrationError> {
292 let json_str = serde_json::to_string(&toml_value)
293 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
294 let json_value: serde_json::Value = serde_json::from_str(&json_str)
295 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
296 Ok(json_value)
297}
298
299#[cfg(feature = "async")]
304pub use async_impl::AsyncDirStorage;
305
306#[cfg(feature = "async")]
307mod async_impl {
308 use crate::{AppPaths, MigrationError, Migrator};
309 use std::path::Path;
310
311 use super::{store_err_to_migration, toml_to_json, DirStorageStrategy, FormatStrategy};
312
313 pub struct AsyncDirStorage {
320 inner: local_store::AsyncDirStorage,
322 migrator: Migrator,
324 strategy: DirStorageStrategy,
326 }
327
328 impl AsyncDirStorage {
329 pub async fn new(
339 paths: AppPaths,
340 domain_name: &str,
341 migrator: Migrator,
342 strategy: DirStorageStrategy,
343 ) -> Result<Self, MigrationError> {
344 let inner = local_store::AsyncDirStorage::new(paths, domain_name, strategy.clone())
345 .await
346 .map_err(store_err_to_migration)?;
347 Ok(Self {
348 inner,
349 migrator,
350 strategy,
351 })
352 }
353
354 pub async fn save<T>(
364 &self,
365 entity_name: &str,
366 id: &str,
367 entity: T,
368 ) -> Result<(), MigrationError>
369 where
370 T: serde::Serialize,
371 {
372 let json_string = self.migrator.save_domain_flat(entity_name, entity)?;
373
374 let versioned_value: serde_json::Value = serde_json::from_str(&json_string)
375 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
376
377 let content = self.serialize_content(&versioned_value)?;
378
379 self.inner
380 .save_raw_string(entity_name, id, &content)
381 .await
382 .map_err(store_err_to_migration)
383 }
384
385 pub async fn load<D>(&self, entity_name: &str, id: &str) -> Result<D, MigrationError>
395 where
396 D: serde::de::DeserializeOwned,
397 {
398 let content = self
399 .inner
400 .load_raw_string(id)
401 .await
402 .map_err(store_err_to_migration)?;
403
404 let value = self.deserialize_content(&content)?;
405 self.migrator.load_flat_from(entity_name, value)
406 }
407
408 pub async fn list_ids(&self) -> Result<Vec<String>, MigrationError> {
414 let mut ids = self
415 .inner
416 .list_ids()
417 .await
418 .map_err(store_err_to_migration)?;
419 ids.sort();
420 Ok(ids)
421 }
422
423 pub async fn load_all<D>(
429 &self,
430 entity_name: &str,
431 ) -> Result<Vec<(String, D)>, MigrationError>
432 where
433 D: serde::de::DeserializeOwned,
434 {
435 let ids = self.list_ids().await?;
436 let mut results = Vec::new();
437 for id in ids {
438 let entity = self.load(entity_name, &id).await?;
439 results.push((id, entity));
440 }
441 Ok(results)
442 }
443
444 pub async fn exists(&self, id: &str) -> Result<bool, MigrationError> {
450 self.inner.exists(id).await.map_err(store_err_to_migration)
451 }
452
453 pub async fn delete(&self, id: &str) -> Result<(), MigrationError> {
461 self.inner.delete(id).await.map_err(store_err_to_migration)
462 }
463
464 pub fn base_path(&self) -> &Path {
466 self.inner.base_path()
467 }
468
469 fn serialize_content(&self, value: &serde_json::Value) -> Result<String, MigrationError> {
474 match self.strategy.format {
475 FormatStrategy::Json => serde_json::to_string_pretty(value)
476 .map_err(|e| MigrationError::SerializationError(e.to_string())),
477 FormatStrategy::Toml => {
478 let tv = local_store::format_convert::json_to_toml(value).map_err(|e| {
479 MigrationError::Store(local_store::StoreError::FormatConvert(e))
480 })?;
481 toml::to_string_pretty(&tv)
482 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))
483 }
484 }
485 }
486
487 fn deserialize_content(&self, content: &str) -> Result<serde_json::Value, MigrationError> {
488 match self.strategy.format {
489 FormatStrategy::Json => serde_json::from_str(content)
490 .map_err(|e| MigrationError::DeserializationError(e.to_string())),
491 FormatStrategy::Toml => {
492 let tv: toml::Value = toml::from_str(content)
493 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
494 toml_to_json(tv)
495 }
496 }
497 }
498 }
499
500 #[cfg(all(test, feature = "async"))]
502 mod async_tests {
503 use super::*;
504 use crate::{FromDomain, IntoDomain, MigratesTo, Versioned};
505 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
506 use base64::Engine;
507 use serde::{Deserialize, Serialize};
508 use tempfile::TempDir;
509
510 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
512 struct SessionV1_0_0 {
513 id: String,
514 user_id: String,
515 }
516
517 impl Versioned for SessionV1_0_0 {
518 const VERSION: &'static str = "1.0.0";
519 }
520
521 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
522 struct SessionV1_1_0 {
523 id: String,
524 user_id: String,
525 created_at: Option<String>,
526 }
527
528 impl Versioned for SessionV1_1_0 {
529 const VERSION: &'static str = "1.1.0";
530 }
531
532 impl MigratesTo<SessionV1_1_0> for SessionV1_0_0 {
533 fn migrate(self) -> SessionV1_1_0 {
534 SessionV1_1_0 {
535 id: self.id,
536 user_id: self.user_id,
537 created_at: None,
538 }
539 }
540 }
541
542 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
543 struct SessionEntity {
544 id: String,
545 user_id: String,
546 created_at: Option<String>,
547 }
548
549 impl IntoDomain<SessionEntity> for SessionV1_1_0 {
550 fn into_domain(self) -> SessionEntity {
551 SessionEntity {
552 id: self.id,
553 user_id: self.user_id,
554 created_at: self.created_at,
555 }
556 }
557 }
558
559 impl FromDomain<SessionEntity> for SessionV1_1_0 {
560 fn from_domain(domain: SessionEntity) -> Self {
561 SessionV1_1_0 {
562 id: domain.id,
563 user_id: domain.user_id,
564 created_at: domain.created_at,
565 }
566 }
567 }
568
569 fn setup_session_migrator() -> Migrator {
570 let path = Migrator::define("session")
571 .from::<SessionV1_0_0>()
572 .step::<SessionV1_1_0>()
573 .into_with_save::<SessionEntity>();
574
575 let mut migrator = Migrator::new();
576 migrator.register(path).unwrap();
577 migrator
578 }
579
580 #[tokio::test]
581 async fn test_async_dir_storage_new_creates_directory() {
582 let temp_dir = TempDir::new().unwrap();
583 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
584 temp_dir.path().to_path_buf(),
585 ));
586
587 let migrator = Migrator::new();
588 let strategy = DirStorageStrategy::default();
589
590 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
591 .await
592 .unwrap();
593
594 assert!(storage.base_path().exists());
596 assert!(storage.base_path().is_dir());
597 assert!(storage.base_path().ends_with("data/testapp/sessions"));
598 }
599
600 #[tokio::test]
601 async fn test_async_dir_storage_save_and_load_roundtrip() {
602 let temp_dir = TempDir::new().unwrap();
603 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
604 temp_dir.path().to_path_buf(),
605 ));
606
607 let migrator = setup_session_migrator();
608 let strategy = DirStorageStrategy::default();
609 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
610 .await
611 .unwrap();
612
613 let sessions = vec![
615 SessionEntity {
616 id: "session-1".to_string(),
617 user_id: "user-1".to_string(),
618 created_at: Some("2024-01-01".to_string()),
619 },
620 SessionEntity {
621 id: "session-2".to_string(),
622 user_id: "user-2".to_string(),
623 created_at: None,
624 },
625 SessionEntity {
626 id: "session-3".to_string(),
627 user_id: "user-3".to_string(),
628 created_at: Some("2024-03-01".to_string()),
629 },
630 ];
631
632 for session in &sessions {
634 storage
635 .save("session", &session.id, session.clone())
636 .await
637 .unwrap();
638 }
639
640 for session in &sessions {
642 let loaded: SessionEntity = storage.load("session", &session.id).await.unwrap();
643 assert_eq!(loaded.id, session.id);
644 assert_eq!(loaded.user_id, session.user_id);
645 assert_eq!(loaded.created_at, session.created_at);
646 }
647 }
648
649 #[tokio::test]
650 async fn test_async_dir_storage_list_ids() {
651 let temp_dir = TempDir::new().unwrap();
652 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
653 temp_dir.path().to_path_buf(),
654 ));
655
656 let migrator = setup_session_migrator();
657 let strategy = DirStorageStrategy::default();
658 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
659 .await
660 .unwrap();
661
662 let ids = vec!["session-c", "session-a", "session-b"];
664 for id in &ids {
665 let session = SessionEntity {
666 id: id.to_string(),
667 user_id: "user".to_string(),
668 created_at: None,
669 };
670 storage.save("session", id, session).await.unwrap();
671 }
672
673 let listed_ids = storage.list_ids().await.unwrap();
675 assert_eq!(listed_ids.len(), 3);
676 assert_eq!(listed_ids, vec!["session-a", "session-b", "session-c"]);
678 }
679
680 #[tokio::test]
681 async fn test_async_dir_storage_load_all() {
682 let temp_dir = TempDir::new().unwrap();
683 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
684 temp_dir.path().to_path_buf(),
685 ));
686
687 let migrator = setup_session_migrator();
688 let strategy = DirStorageStrategy::default();
689 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
690 .await
691 .unwrap();
692
693 let sessions = vec![
695 SessionEntity {
696 id: "session-x".to_string(),
697 user_id: "user-x".to_string(),
698 created_at: Some("2024-01-01".to_string()),
699 },
700 SessionEntity {
701 id: "session-y".to_string(),
702 user_id: "user-y".to_string(),
703 created_at: None,
704 },
705 SessionEntity {
706 id: "session-z".to_string(),
707 user_id: "user-z".to_string(),
708 created_at: Some("2024-03-01".to_string()),
709 },
710 ];
711
712 for session in &sessions {
713 storage
714 .save("session", &session.id, session.clone())
715 .await
716 .unwrap();
717 }
718
719 let results: Vec<(String, SessionEntity)> = storage.load_all("session").await.unwrap();
721 assert_eq!(results.len(), 3);
722
723 for (id, loaded) in &results {
725 let original = sessions.iter().find(|s| &s.id == id).unwrap();
726 assert_eq!(loaded.id, original.id);
727 assert_eq!(loaded.user_id, original.user_id);
728 assert_eq!(loaded.created_at, original.created_at);
729 }
730 }
731
732 #[tokio::test]
733 async fn test_async_dir_storage_delete() {
734 let temp_dir = TempDir::new().unwrap();
735 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
736 temp_dir.path().to_path_buf(),
737 ));
738
739 let migrator = setup_session_migrator();
740 let strategy = DirStorageStrategy::default();
741 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
742 .await
743 .unwrap();
744
745 let session = SessionEntity {
747 id: "session-delete".to_string(),
748 user_id: "user-delete".to_string(),
749 created_at: None,
750 };
751 storage
752 .save("session", "session-delete", session)
753 .await
754 .unwrap();
755
756 assert!(storage.exists("session-delete").await.unwrap());
758
759 storage.delete("session-delete").await.unwrap();
761
762 assert!(!storage.exists("session-delete").await.unwrap());
764 }
765
766 #[tokio::test]
767 async fn test_async_dir_storage_filename_encoding_url_roundtrip() {
768 let temp_dir = TempDir::new().unwrap();
769 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
770 temp_dir.path().to_path_buf(),
771 ));
772
773 let migrator = setup_session_migrator();
774 let strategy =
775 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::UrlEncode);
776 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
777 .await
778 .unwrap();
779
780 let complex_id = "user@example.com/path?query=1";
782 let session = SessionEntity {
783 id: complex_id.to_string(),
784 user_id: "user-special".to_string(),
785 created_at: Some("2024-05-01".to_string()),
786 };
787
788 storage
790 .save("session", complex_id, session.clone())
791 .await
792 .unwrap();
793
794 let encoded_id = urlencoding::encode(complex_id);
796 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
797 assert!(file_path.exists());
798
799 let loaded: SessionEntity = storage.load("session", complex_id).await.unwrap();
801 assert_eq!(loaded.id, session.id);
802 assert_eq!(loaded.user_id, session.user_id);
803 assert_eq!(loaded.created_at, session.created_at);
804
805 let ids = storage.list_ids().await.unwrap();
807 assert_eq!(ids.len(), 1);
808 assert_eq!(ids[0], complex_id);
809 }
810
811 #[tokio::test]
812 async fn test_async_dir_storage_filename_encoding_base64_roundtrip() {
813 let temp_dir = TempDir::new().unwrap();
814 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
815 temp_dir.path().to_path_buf(),
816 ));
817
818 let migrator = setup_session_migrator();
819 let strategy =
820 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::Base64);
821 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
822 .await
823 .unwrap();
824
825 let complex_id = "user@example.com/path?query=1&special=!@#$%";
827 let session = SessionEntity {
828 id: complex_id.to_string(),
829 user_id: "user-base64".to_string(),
830 created_at: Some("2024-06-01".to_string()),
831 };
832
833 storage
835 .save("session", complex_id, session.clone())
836 .await
837 .unwrap();
838
839 let encoded_id = URL_SAFE_NO_PAD.encode(complex_id.as_bytes());
841 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
842 assert!(file_path.exists());
843
844 let loaded: SessionEntity = storage.load("session", complex_id).await.unwrap();
846 assert_eq!(loaded.id, session.id);
847 assert_eq!(loaded.user_id, session.user_id);
848 assert_eq!(loaded.created_at, session.created_at);
849
850 let ids = storage.list_ids().await.unwrap();
852 assert_eq!(ids.len(), 1);
853 assert_eq!(ids[0], complex_id);
854 }
855 }
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
862 use base64::Engine;
863 use local_store::StoreError;
864 use std::fs;
865 use tempfile::TempDir;
866
867 #[test]
868 fn test_filename_encoding_default() {
869 assert_eq!(FilenameEncoding::default(), FilenameEncoding::Direct);
870 }
871
872 #[test]
873 fn test_dir_storage_strategy_default() {
874 let strategy = DirStorageStrategy::default();
875 assert_eq!(strategy.format, FormatStrategy::Json);
876 assert_eq!(strategy.extension, None);
877 assert_eq!(strategy.filename_encoding, FilenameEncoding::Direct);
878 }
879
880 #[test]
881 fn test_dir_storage_strategy_builder() {
882 let strategy = DirStorageStrategy::new()
883 .with_format(FormatStrategy::Toml)
884 .with_extension("data")
885 .with_filename_encoding(FilenameEncoding::Base64)
886 .with_retry_count(5)
887 .with_cleanup(false);
888
889 assert_eq!(strategy.format, FormatStrategy::Toml);
890 assert_eq!(strategy.extension, Some("data".to_string()));
891 assert_eq!(strategy.filename_encoding, FilenameEncoding::Base64);
892 assert_eq!(strategy.atomic_write.retry_count, 5);
893 assert!(!strategy.atomic_write.cleanup_tmp_files);
894 }
895
896 #[test]
897 fn test_dir_storage_strategy_get_extension() {
898 let strategy1 = DirStorageStrategy::default();
900 assert_eq!(strategy1.get_extension(), "json");
901
902 let strategy2 = DirStorageStrategy::default().with_format(FormatStrategy::Toml);
904 assert_eq!(strategy2.get_extension(), "toml");
905
906 let strategy3 = DirStorageStrategy::default().with_extension("custom");
908 assert_eq!(strategy3.get_extension(), "custom");
909 }
910
911 #[test]
912 fn test_dir_storage_new_creates_directory() {
913 let temp_dir = TempDir::new().unwrap();
914 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
915 temp_dir.path().to_path_buf(),
916 ));
917
918 let migrator = Migrator::new();
919 let strategy = DirStorageStrategy::default();
920
921 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
922
923 assert!(storage.base_path().exists());
925 assert!(storage.base_path().is_dir());
926 assert!(storage.base_path().ends_with("data/testapp/sessions"));
927 }
928
929 #[test]
930 fn test_dir_storage_new_idempotent() {
931 let temp_dir = TempDir::new().unwrap();
932 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
933 temp_dir.path().to_path_buf(),
934 ));
935
936 let migrator1 = Migrator::new();
937 let migrator2 = Migrator::new();
938 let strategy = DirStorageStrategy::default();
939
940 let storage1 =
942 DirStorage::new(paths.clone(), "sessions", migrator1, strategy.clone()).unwrap();
943 let storage2 = DirStorage::new(paths, "sessions", migrator2, strategy).unwrap();
944
945 assert_eq!(storage1.base_path(), storage2.base_path());
947 }
948
949 use crate::{FromDomain, IntoDomain, MigratesTo, Versioned};
951 use serde::{Deserialize, Serialize};
952
953 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
954 struct SessionV1_0_0 {
955 id: String,
956 user_id: String,
957 }
958
959 impl Versioned for SessionV1_0_0 {
960 const VERSION: &'static str = "1.0.0";
961 }
962
963 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
964 struct SessionV1_1_0 {
965 id: String,
966 user_id: String,
967 created_at: Option<String>,
968 }
969
970 impl Versioned for SessionV1_1_0 {
971 const VERSION: &'static str = "1.1.0";
972 }
973
974 impl MigratesTo<SessionV1_1_0> for SessionV1_0_0 {
975 fn migrate(self) -> SessionV1_1_0 {
976 SessionV1_1_0 {
977 id: self.id,
978 user_id: self.user_id,
979 created_at: None,
980 }
981 }
982 }
983
984 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
985 struct SessionEntity {
986 id: String,
987 user_id: String,
988 created_at: Option<String>,
989 }
990
991 impl IntoDomain<SessionEntity> for SessionV1_1_0 {
992 fn into_domain(self) -> SessionEntity {
993 SessionEntity {
994 id: self.id,
995 user_id: self.user_id,
996 created_at: self.created_at,
997 }
998 }
999 }
1000
1001 impl FromDomain<SessionEntity> for SessionV1_1_0 {
1002 fn from_domain(domain: SessionEntity) -> Self {
1003 SessionV1_1_0 {
1004 id: domain.id,
1005 user_id: domain.user_id,
1006 created_at: domain.created_at,
1007 }
1008 }
1009 }
1010
1011 fn setup_session_migrator() -> Migrator {
1012 let path = Migrator::define("session")
1013 .from::<SessionV1_0_0>()
1014 .step::<SessionV1_1_0>()
1015 .into_with_save::<SessionEntity>();
1016
1017 let mut migrator = Migrator::new();
1018 migrator.register(path).unwrap();
1019 migrator
1020 }
1021
1022 #[test]
1023 fn test_dir_storage_save_json() {
1024 let temp_dir = TempDir::new().unwrap();
1025 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1026 temp_dir.path().to_path_buf(),
1027 ));
1028
1029 let migrator = setup_session_migrator();
1030 let strategy = DirStorageStrategy::default().with_format(FormatStrategy::Json);
1031 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1032
1033 let session = SessionEntity {
1035 id: "session-123".to_string(),
1036 user_id: "user-456".to_string(),
1037 created_at: Some("2024-01-01T00:00:00Z".to_string()),
1038 };
1039
1040 storage.save("session", "session-123", session).unwrap();
1042
1043 let file_path = storage.base_path().join("session-123.json");
1045 assert!(file_path.exists());
1046
1047 let content = std::fs::read_to_string(&file_path).unwrap();
1049 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
1050 assert_eq!(json["version"], "1.1.0");
1051 assert_eq!(json["id"], "session-123");
1052 assert_eq!(json["user_id"], "user-456");
1053 }
1054
1055 #[test]
1056 fn test_dir_storage_save_toml() {
1057 let temp_dir = TempDir::new().unwrap();
1058 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1059 temp_dir.path().to_path_buf(),
1060 ));
1061
1062 let migrator = setup_session_migrator();
1063 let strategy = DirStorageStrategy::default().with_format(FormatStrategy::Toml);
1064 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1065
1066 let session = SessionEntity {
1068 id: "session-789".to_string(),
1069 user_id: "user-101".to_string(),
1070 created_at: Some("2024-01-15T10:30:00Z".to_string()),
1071 };
1072
1073 storage.save("session", "session-789", session).unwrap();
1075
1076 let file_path = storage.base_path().join("session-789.toml");
1078 assert!(file_path.exists());
1079
1080 let content = std::fs::read_to_string(&file_path).unwrap();
1082 let toml: toml::Value = toml::from_str(&content).unwrap();
1083 assert_eq!(toml["version"].as_str().unwrap(), "1.1.0");
1084 assert_eq!(toml["id"].as_str().unwrap(), "session-789");
1085 assert_eq!(toml["created_at"].as_str().unwrap(), "2024-01-15T10:30:00Z");
1086 }
1087
1088 #[test]
1089 fn test_dir_storage_save_with_invalid_id() {
1090 let temp_dir = TempDir::new().unwrap();
1091 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1092 temp_dir.path().to_path_buf(),
1093 ));
1094
1095 let migrator = setup_session_migrator();
1096 let strategy = DirStorageStrategy::default();
1097 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1098
1099 let session = SessionEntity {
1100 id: "invalid/id".to_string(),
1101 user_id: "user-456".to_string(),
1102 created_at: None,
1103 };
1104
1105 let result = storage.save("session", "invalid/id", session);
1107 assert!(result.is_err());
1108 assert!(matches!(
1109 result.unwrap_err(),
1110 crate::MigrationError::FilenameEncoding { .. }
1111 ));
1112 }
1113
1114 #[test]
1115 fn test_dir_storage_save_with_custom_extension() {
1116 let temp_dir = TempDir::new().unwrap();
1117 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1118 temp_dir.path().to_path_buf(),
1119 ));
1120
1121 let migrator = setup_session_migrator();
1122 let strategy = DirStorageStrategy::default()
1123 .with_format(FormatStrategy::Json)
1124 .with_extension("data");
1125 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1126
1127 let session = SessionEntity {
1128 id: "session-custom".to_string(),
1129 user_id: "user-999".to_string(),
1130 created_at: None,
1131 };
1132
1133 storage.save("session", "session-custom", session).unwrap();
1134
1135 let file_path = storage.base_path().join("session-custom.data");
1137 assert!(file_path.exists());
1138 }
1139
1140 #[test]
1141 fn test_dir_storage_save_overwrites_existing() {
1142 let temp_dir = TempDir::new().unwrap();
1143 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1144 temp_dir.path().to_path_buf(),
1145 ));
1146
1147 let migrator = setup_session_migrator();
1148 let strategy = DirStorageStrategy::default();
1149 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1150
1151 let session1 = SessionEntity {
1153 id: "session-overwrite".to_string(),
1154 user_id: "user-111".to_string(),
1155 created_at: Some("2024-01-01".to_string()),
1156 };
1157 storage
1158 .save("session", "session-overwrite", session1)
1159 .unwrap();
1160
1161 let session2 = SessionEntity {
1163 id: "session-overwrite".to_string(),
1164 user_id: "user-222".to_string(),
1165 created_at: Some("2024-01-02".to_string()),
1166 };
1167 storage
1168 .save("session", "session-overwrite", session2)
1169 .unwrap();
1170
1171 let file_path = storage.base_path().join("session-overwrite.json");
1173 let content = std::fs::read_to_string(&file_path).unwrap();
1174 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
1175 assert_eq!(json["user_id"], "user-222");
1176 assert_eq!(json["created_at"], "2024-01-02");
1177 }
1178
1179 #[test]
1180 fn test_dir_storage_load_success() {
1181 let temp_dir = TempDir::new().unwrap();
1182 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1183 temp_dir.path().to_path_buf(),
1184 ));
1185
1186 let migrator = setup_session_migrator();
1187 let strategy = DirStorageStrategy::default();
1188 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1189
1190 let session = SessionEntity {
1192 id: "session-load".to_string(),
1193 user_id: "user-999".to_string(),
1194 created_at: Some("2024-02-01".to_string()),
1195 };
1196 storage
1197 .save("session", "session-load", session.clone())
1198 .unwrap();
1199
1200 let loaded: SessionEntity = storage.load("session", "session-load").unwrap();
1202 assert_eq!(loaded.id, session.id);
1203 assert_eq!(loaded.user_id, session.user_id);
1204 assert_eq!(loaded.created_at, session.created_at);
1205 }
1206
1207 #[test]
1208 fn test_dir_storage_load_not_found() {
1209 let temp_dir = TempDir::new().unwrap();
1210 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1211 temp_dir.path().to_path_buf(),
1212 ));
1213
1214 let migrator = setup_session_migrator();
1215 let strategy = DirStorageStrategy::default();
1216 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1217
1218 let result: Result<SessionEntity, _> = storage.load("session", "non-existent");
1220 assert!(result.is_err());
1221 assert!(matches!(
1222 result.unwrap_err(),
1223 MigrationError::Store(StoreError::IoError { .. })
1224 ));
1225 }
1226
1227 #[test]
1228 fn test_dir_storage_save_and_load_roundtrip() {
1229 let temp_dir = TempDir::new().unwrap();
1230 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1231 temp_dir.path().to_path_buf(),
1232 ));
1233
1234 let migrator = setup_session_migrator();
1235 let strategy = DirStorageStrategy::default();
1236 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1237
1238 let sessions = vec![
1240 SessionEntity {
1241 id: "session-1".to_string(),
1242 user_id: "user-1".to_string(),
1243 created_at: Some("2024-01-01".to_string()),
1244 },
1245 SessionEntity {
1246 id: "session-2".to_string(),
1247 user_id: "user-2".to_string(),
1248 created_at: None,
1249 },
1250 SessionEntity {
1251 id: "session-3".to_string(),
1252 user_id: "user-3".to_string(),
1253 created_at: Some("2024-03-01".to_string()),
1254 },
1255 ];
1256
1257 for session in &sessions {
1259 storage
1260 .save("session", &session.id, session.clone())
1261 .unwrap();
1262 }
1263
1264 for session in &sessions {
1266 let loaded: SessionEntity = storage.load("session", &session.id).unwrap();
1267 assert_eq!(loaded.id, session.id);
1268 assert_eq!(loaded.user_id, session.user_id);
1269 assert_eq!(loaded.created_at, session.created_at);
1270 }
1271 }
1272
1273 #[test]
1274 fn test_dir_storage_list_ids_empty() {
1275 let temp_dir = TempDir::new().unwrap();
1276 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1277 temp_dir.path().to_path_buf(),
1278 ));
1279
1280 let migrator = setup_session_migrator();
1281 let strategy = DirStorageStrategy::default();
1282 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1283
1284 let ids = storage.list_ids().unwrap();
1286 assert!(ids.is_empty());
1287 }
1288
1289 #[test]
1290 fn test_dir_storage_list_ids() {
1291 let temp_dir = TempDir::new().unwrap();
1292 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1293 temp_dir.path().to_path_buf(),
1294 ));
1295
1296 let migrator = setup_session_migrator();
1297 let strategy = DirStorageStrategy::default();
1298 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1299
1300 let ids = vec!["session-c", "session-a", "session-b"];
1302 for id in &ids {
1303 let session = SessionEntity {
1304 id: id.to_string(),
1305 user_id: "user".to_string(),
1306 created_at: None,
1307 };
1308 storage.save("session", id, session).unwrap();
1309 }
1310
1311 let listed_ids = storage.list_ids().unwrap();
1313 assert_eq!(listed_ids.len(), 3);
1314 assert_eq!(listed_ids, vec!["session-a", "session-b", "session-c"]);
1316 }
1317
1318 #[test]
1319 fn test_dir_storage_load_all_empty() {
1320 let temp_dir = TempDir::new().unwrap();
1321 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1322 temp_dir.path().to_path_buf(),
1323 ));
1324
1325 let migrator = setup_session_migrator();
1326 let strategy = DirStorageStrategy::default();
1327 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1328
1329 let results: Vec<(String, SessionEntity)> = storage.load_all("session").unwrap();
1331 assert!(results.is_empty());
1332 }
1333
1334 #[test]
1335 fn test_dir_storage_load_all() {
1336 let temp_dir = TempDir::new().unwrap();
1337 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1338 temp_dir.path().to_path_buf(),
1339 ));
1340
1341 let migrator = setup_session_migrator();
1342 let strategy = DirStorageStrategy::default();
1343 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1344
1345 let sessions = vec![
1347 SessionEntity {
1348 id: "session-x".to_string(),
1349 user_id: "user-x".to_string(),
1350 created_at: Some("2024-01-01".to_string()),
1351 },
1352 SessionEntity {
1353 id: "session-y".to_string(),
1354 user_id: "user-y".to_string(),
1355 created_at: None,
1356 },
1357 SessionEntity {
1358 id: "session-z".to_string(),
1359 user_id: "user-z".to_string(),
1360 created_at: Some("2024-03-01".to_string()),
1361 },
1362 ];
1363
1364 for session in &sessions {
1365 storage
1366 .save("session", &session.id, session.clone())
1367 .unwrap();
1368 }
1369
1370 let results: Vec<(String, SessionEntity)> = storage.load_all("session").unwrap();
1372 assert_eq!(results.len(), 3);
1373
1374 for (id, loaded) in &results {
1376 let original = sessions.iter().find(|s| &s.id == id).unwrap();
1377 assert_eq!(loaded.id, original.id);
1378 assert_eq!(loaded.user_id, original.user_id);
1379 assert_eq!(loaded.created_at, original.created_at);
1380 }
1381 }
1382
1383 #[test]
1384 fn test_dir_storage_exists() {
1385 let temp_dir = TempDir::new().unwrap();
1386 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1387 temp_dir.path().to_path_buf(),
1388 ));
1389
1390 let migrator = setup_session_migrator();
1391 let strategy = DirStorageStrategy::default();
1392 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1393
1394 assert!(!storage.exists("session-exists").unwrap());
1396
1397 let session = SessionEntity {
1399 id: "session-exists".to_string(),
1400 user_id: "user-exists".to_string(),
1401 created_at: None,
1402 };
1403 storage.save("session", "session-exists", session).unwrap();
1404
1405 assert!(storage.exists("session-exists").unwrap());
1407 }
1408
1409 #[test]
1410 fn test_dir_storage_delete() {
1411 let temp_dir = TempDir::new().unwrap();
1412 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1413 temp_dir.path().to_path_buf(),
1414 ));
1415
1416 let migrator = setup_session_migrator();
1417 let strategy = DirStorageStrategy::default();
1418 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1419
1420 let session = SessionEntity {
1422 id: "session-delete".to_string(),
1423 user_id: "user-delete".to_string(),
1424 created_at: None,
1425 };
1426 storage.save("session", "session-delete", session).unwrap();
1427
1428 assert!(storage.exists("session-delete").unwrap());
1430
1431 storage.delete("session-delete").unwrap();
1433
1434 assert!(!storage.exists("session-delete").unwrap());
1436 }
1437
1438 #[test]
1439 fn test_dir_storage_delete_idempotent() {
1440 let temp_dir = TempDir::new().unwrap();
1441 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1442 temp_dir.path().to_path_buf(),
1443 ));
1444
1445 let migrator = setup_session_migrator();
1446 let strategy = DirStorageStrategy::default();
1447 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1448
1449 storage.delete("non-existent").unwrap();
1451
1452 storage.delete("non-existent").unwrap();
1454 }
1455
1456 #[test]
1457 fn test_dir_storage_load_toml() {
1458 let temp_dir = TempDir::new().unwrap();
1459 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1460 temp_dir.path().to_path_buf(),
1461 ));
1462
1463 let migrator = setup_session_migrator();
1464 let strategy = DirStorageStrategy::default().with_format(FormatStrategy::Toml);
1465 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1466
1467 let session = SessionEntity {
1469 id: "session-toml".to_string(),
1470 user_id: "user-toml".to_string(),
1471 created_at: Some("2024-04-01".to_string()),
1472 };
1473 storage
1474 .save("session", "session-toml", session.clone())
1475 .unwrap();
1476
1477 let loaded: SessionEntity = storage.load("session", "session-toml").unwrap();
1479 assert_eq!(loaded.id, session.id);
1480 assert_eq!(loaded.user_id, session.user_id);
1481 assert_eq!(loaded.created_at, session.created_at);
1482 }
1483
1484 #[test]
1485 fn test_dir_storage_list_ids_with_custom_extension() {
1486 let temp_dir = TempDir::new().unwrap();
1487 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1488 temp_dir.path().to_path_buf(),
1489 ));
1490
1491 let migrator = setup_session_migrator();
1492 let strategy = DirStorageStrategy::default().with_extension("data");
1493 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1494
1495 let session = SessionEntity {
1497 id: "session-ext".to_string(),
1498 user_id: "user-ext".to_string(),
1499 created_at: None,
1500 };
1501 storage.save("session", "session-ext", session).unwrap();
1502
1503 let ids = storage.list_ids().unwrap();
1505 assert_eq!(ids.len(), 1);
1506 assert_eq!(ids[0], "session-ext");
1507 }
1508
1509 #[test]
1510 fn test_dir_storage_load_all_atomic_failure() {
1511 let temp_dir = TempDir::new().unwrap();
1512 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1513 temp_dir.path().to_path_buf(),
1514 ));
1515
1516 let migrator = setup_session_migrator();
1517 let strategy = DirStorageStrategy::default();
1518 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1519
1520 let session1 = SessionEntity {
1522 id: "session-1".to_string(),
1523 user_id: "user-1".to_string(),
1524 created_at: None,
1525 };
1526 storage.save("session", "session-1", session1).unwrap();
1527
1528 let corrupted_path = storage.base_path().join("session-corrupted.json");
1530 std::fs::write(&corrupted_path, "invalid json {{{").unwrap();
1531
1532 let result: Result<Vec<(String, SessionEntity)>, _> = storage.load_all("session");
1534 assert!(result.is_err());
1535 }
1536
1537 #[test]
1538 fn test_dir_storage_filename_encoding_url_roundtrip() {
1539 let temp_dir = TempDir::new().unwrap();
1540 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1541 temp_dir.path().to_path_buf(),
1542 ));
1543
1544 let migrator = setup_session_migrator();
1545 let strategy =
1546 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::UrlEncode);
1547 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1548
1549 let complex_id = "user@example.com/path?query=1";
1551 let session = SessionEntity {
1552 id: complex_id.to_string(),
1553 user_id: "user-special".to_string(),
1554 created_at: Some("2024-05-01".to_string()),
1555 };
1556
1557 storage
1559 .save("session", complex_id, session.clone())
1560 .unwrap();
1561
1562 let encoded_id = urlencoding::encode(complex_id);
1564 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
1565 assert!(file_path.exists());
1566
1567 let loaded: SessionEntity = storage.load("session", complex_id).unwrap();
1569 assert_eq!(loaded.id, session.id);
1570 assert_eq!(loaded.user_id, session.user_id);
1571 assert_eq!(loaded.created_at, session.created_at);
1572
1573 let ids = storage.list_ids().unwrap();
1575 assert_eq!(ids.len(), 1);
1576 assert_eq!(ids[0], complex_id);
1577 }
1578
1579 #[test]
1580 fn test_dir_storage_filename_encoding_base64_roundtrip() {
1581 let temp_dir = TempDir::new().unwrap();
1582 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1583 temp_dir.path().to_path_buf(),
1584 ));
1585
1586 let migrator = setup_session_migrator();
1587 let strategy =
1588 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::Base64);
1589 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1590
1591 let complex_id = "user@example.com/path?query=1&special=!@#$%";
1593 let session = SessionEntity {
1594 id: complex_id.to_string(),
1595 user_id: "user-base64".to_string(),
1596 created_at: Some("2024-06-01".to_string()),
1597 };
1598
1599 storage
1601 .save("session", complex_id, session.clone())
1602 .unwrap();
1603
1604 let encoded_id = URL_SAFE_NO_PAD.encode(complex_id.as_bytes());
1606 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
1607 assert!(file_path.exists());
1608
1609 let loaded: SessionEntity = storage.load("session", complex_id).unwrap();
1611 assert_eq!(loaded.id, session.id);
1612 assert_eq!(loaded.user_id, session.user_id);
1613 assert_eq!(loaded.created_at, session.created_at);
1614
1615 let ids = storage.list_ids().unwrap();
1617 assert_eq!(ids.len(), 1);
1618 assert_eq!(ids[0], complex_id);
1619 }
1620
1621 #[test]
1625 fn test_list_ids_url_decode_error() {
1626 let temp_dir = TempDir::new().unwrap();
1627 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1628 temp_dir.path().to_path_buf(),
1629 ));
1630
1631 let migrator = setup_session_migrator();
1632 let strategy =
1633 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::UrlEncode);
1634 let storage = DirStorage::new(paths, "sessions_url_err", migrator, strategy).unwrap();
1635
1636 let bad_stem = "%C0%C1";
1639 let bad_file = storage.base_path().join(format!("{}.json", bad_stem));
1640 std::fs::write(&bad_file, "{}").unwrap();
1641
1642 let result = storage.list_ids();
1643 assert!(
1644 result.is_err(),
1645 "list_ids should propagate the FilenameEncoding decode error"
1646 );
1647 assert!(matches!(
1648 result.unwrap_err(),
1649 MigrationError::FilenameEncoding { .. }
1650 ));
1651 }
1652
1653 #[test]
1656 fn test_list_ids_base64_decode_error() {
1657 let temp_dir = TempDir::new().unwrap();
1658 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1659 temp_dir.path().to_path_buf(),
1660 ));
1661
1662 let migrator = setup_session_migrator();
1663 let strategy =
1664 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::Base64);
1665 let storage = DirStorage::new(paths, "sessions_b64_err", migrator, strategy).unwrap();
1666
1667 let bad_stem = "!!!invalid@@@";
1669 let bad_file = storage.base_path().join(format!("{}.json", bad_stem));
1670 std::fs::write(&bad_file, "{}").unwrap();
1671
1672 let result = storage.list_ids();
1673 assert!(
1674 result.is_err(),
1675 "list_ids should propagate the FilenameEncoding decode error"
1676 );
1677 assert!(matches!(
1678 result.unwrap_err(),
1679 MigrationError::FilenameEncoding { .. }
1680 ));
1681 }
1682
1683 #[test]
1684 fn test_dir_storage_base_path() {
1685 let temp_dir = TempDir::new().unwrap();
1686 let domain_name = "test_sessions";
1687 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1688 temp_dir.path().to_path_buf(),
1689 ));
1690
1691 let migrator = Migrator::new();
1692 let strategy = DirStorageStrategy::default();
1693
1694 let storage = DirStorage::new(paths, domain_name, migrator, strategy).unwrap();
1695
1696 let returned_path = storage.base_path();
1698 assert!(returned_path.ends_with(domain_name));
1699 assert!(returned_path.exists());
1700 }
1701
1702 #[test]
1703 #[cfg(unix)]
1704 fn test_dir_storage_create_dir_permission_denied() {
1705 use std::os::unix::fs::PermissionsExt;
1706
1707 let temp_dir = TempDir::new().unwrap();
1708
1709 let mut perms = fs::metadata(temp_dir.path()).unwrap().permissions();
1711 perms.set_mode(0o444);
1712 fs::set_permissions(temp_dir.path(), perms).unwrap();
1713
1714 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1715 temp_dir.path().to_path_buf(),
1716 ));
1717
1718 let migrator = Migrator::new();
1719 let strategy = DirStorageStrategy::default();
1720
1721 let result = DirStorage::new(paths, "sessions", migrator, strategy);
1723
1724 let mut perms = fs::metadata(temp_dir.path()).unwrap().permissions();
1726 perms.set_mode(0o755);
1727 fs::set_permissions(temp_dir.path(), perms).unwrap();
1728
1729 assert!(result.is_err());
1730 assert!(matches!(
1731 result,
1732 Err(MigrationError::Store(StoreError::IoError { .. }))
1733 ));
1734 }
1735
1736 #[test]
1737 #[cfg(unix)]
1738 fn test_dir_storage_save_permission_denied() {
1739 use std::os::unix::fs::PermissionsExt;
1740
1741 let temp_dir = TempDir::new().unwrap();
1742 let domain_name = "sessions_readonly";
1743 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1744 temp_dir.path().to_path_buf(),
1745 ));
1746
1747 let migrator = setup_session_migrator();
1748 let strategy = DirStorageStrategy::default();
1749
1750 let storage = DirStorage::new(paths, domain_name, migrator, strategy).unwrap();
1752
1753 let mut perms = fs::metadata(storage.base_path()).unwrap().permissions();
1755 perms.set_mode(0o444);
1756 fs::set_permissions(storage.base_path(), perms).unwrap();
1757
1758 let session = SessionEntity {
1759 id: "test".to_string(),
1760 user_id: "user".to_string(),
1761 created_at: None,
1762 };
1763
1764 let result = storage.save("session", "test-session", session);
1766
1767 let mut perms = fs::metadata(storage.base_path()).unwrap().permissions();
1769 perms.set_mode(0o755);
1770 fs::set_permissions(storage.base_path(), perms).unwrap();
1771
1772 assert!(result.is_err());
1773 assert!(matches!(
1774 result,
1775 Err(MigrationError::Store(StoreError::IoError { .. }))
1776 ));
1777 }
1778}