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 local_store::FilenameEncoding;
508 use serde::{Deserialize, Serialize};
509 use tempfile::TempDir;
510
511 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
513 struct SessionV1_0_0 {
514 id: String,
515 user_id: String,
516 }
517
518 impl Versioned for SessionV1_0_0 {
519 const VERSION: &'static str = "1.0.0";
520 }
521
522 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
523 struct SessionV1_1_0 {
524 id: String,
525 user_id: String,
526 created_at: Option<String>,
527 }
528
529 impl Versioned for SessionV1_1_0 {
530 const VERSION: &'static str = "1.1.0";
531 }
532
533 impl MigratesTo<SessionV1_1_0> for SessionV1_0_0 {
534 fn migrate(self) -> SessionV1_1_0 {
535 SessionV1_1_0 {
536 id: self.id,
537 user_id: self.user_id,
538 created_at: None,
539 }
540 }
541 }
542
543 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
544 struct SessionEntity {
545 id: String,
546 user_id: String,
547 created_at: Option<String>,
548 }
549
550 impl IntoDomain<SessionEntity> for SessionV1_1_0 {
551 fn into_domain(self) -> SessionEntity {
552 SessionEntity {
553 id: self.id,
554 user_id: self.user_id,
555 created_at: self.created_at,
556 }
557 }
558 }
559
560 impl FromDomain<SessionEntity> for SessionV1_1_0 {
561 fn from_domain(domain: SessionEntity) -> Self {
562 SessionV1_1_0 {
563 id: domain.id,
564 user_id: domain.user_id,
565 created_at: domain.created_at,
566 }
567 }
568 }
569
570 fn setup_session_migrator() -> Migrator {
571 let path = Migrator::define("session")
572 .from::<SessionV1_0_0>()
573 .step::<SessionV1_1_0>()
574 .into_with_save::<SessionEntity>();
575
576 let mut migrator = Migrator::new();
577 migrator.register(path).unwrap();
578 migrator
579 }
580
581 #[tokio::test]
582 async fn test_async_dir_storage_new_creates_directory() {
583 let temp_dir = TempDir::new().unwrap();
584 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
585 temp_dir.path().to_path_buf(),
586 ));
587
588 let migrator = Migrator::new();
589 let strategy = DirStorageStrategy::default();
590
591 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
592 .await
593 .unwrap();
594
595 assert!(storage.base_path().exists());
597 assert!(storage.base_path().is_dir());
598 assert!(storage.base_path().ends_with("data/testapp/sessions"));
599 }
600
601 #[tokio::test]
602 async fn test_async_dir_storage_save_and_load_roundtrip() {
603 let temp_dir = TempDir::new().unwrap();
604 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
605 temp_dir.path().to_path_buf(),
606 ));
607
608 let migrator = setup_session_migrator();
609 let strategy = DirStorageStrategy::default();
610 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
611 .await
612 .unwrap();
613
614 let sessions = vec![
616 SessionEntity {
617 id: "session-1".to_string(),
618 user_id: "user-1".to_string(),
619 created_at: Some("2024-01-01".to_string()),
620 },
621 SessionEntity {
622 id: "session-2".to_string(),
623 user_id: "user-2".to_string(),
624 created_at: None,
625 },
626 SessionEntity {
627 id: "session-3".to_string(),
628 user_id: "user-3".to_string(),
629 created_at: Some("2024-03-01".to_string()),
630 },
631 ];
632
633 for session in &sessions {
635 storage
636 .save("session", &session.id, session.clone())
637 .await
638 .unwrap();
639 }
640
641 for session in &sessions {
643 let loaded: SessionEntity = storage.load("session", &session.id).await.unwrap();
644 assert_eq!(loaded.id, session.id);
645 assert_eq!(loaded.user_id, session.user_id);
646 assert_eq!(loaded.created_at, session.created_at);
647 }
648 }
649
650 #[tokio::test]
651 async fn test_async_dir_storage_list_ids() {
652 let temp_dir = TempDir::new().unwrap();
653 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
654 temp_dir.path().to_path_buf(),
655 ));
656
657 let migrator = setup_session_migrator();
658 let strategy = DirStorageStrategy::default();
659 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
660 .await
661 .unwrap();
662
663 let ids = vec!["session-c", "session-a", "session-b"];
665 for id in &ids {
666 let session = SessionEntity {
667 id: id.to_string(),
668 user_id: "user".to_string(),
669 created_at: None,
670 };
671 storage.save("session", id, session).await.unwrap();
672 }
673
674 let listed_ids = storage.list_ids().await.unwrap();
676 assert_eq!(listed_ids.len(), 3);
677 assert_eq!(listed_ids, vec!["session-a", "session-b", "session-c"]);
679 }
680
681 #[tokio::test]
682 async fn test_async_dir_storage_load_all() {
683 let temp_dir = TempDir::new().unwrap();
684 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
685 temp_dir.path().to_path_buf(),
686 ));
687
688 let migrator = setup_session_migrator();
689 let strategy = DirStorageStrategy::default();
690 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
691 .await
692 .unwrap();
693
694 let sessions = vec![
696 SessionEntity {
697 id: "session-x".to_string(),
698 user_id: "user-x".to_string(),
699 created_at: Some("2024-01-01".to_string()),
700 },
701 SessionEntity {
702 id: "session-y".to_string(),
703 user_id: "user-y".to_string(),
704 created_at: None,
705 },
706 SessionEntity {
707 id: "session-z".to_string(),
708 user_id: "user-z".to_string(),
709 created_at: Some("2024-03-01".to_string()),
710 },
711 ];
712
713 for session in &sessions {
714 storage
715 .save("session", &session.id, session.clone())
716 .await
717 .unwrap();
718 }
719
720 let results: Vec<(String, SessionEntity)> = storage.load_all("session").await.unwrap();
722 assert_eq!(results.len(), 3);
723
724 for (id, loaded) in &results {
726 let original = sessions.iter().find(|s| &s.id == id).unwrap();
727 assert_eq!(loaded.id, original.id);
728 assert_eq!(loaded.user_id, original.user_id);
729 assert_eq!(loaded.created_at, original.created_at);
730 }
731 }
732
733 #[tokio::test]
734 async fn test_async_dir_storage_delete() {
735 let temp_dir = TempDir::new().unwrap();
736 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
737 temp_dir.path().to_path_buf(),
738 ));
739
740 let migrator = setup_session_migrator();
741 let strategy = DirStorageStrategy::default();
742 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
743 .await
744 .unwrap();
745
746 let session = SessionEntity {
748 id: "session-delete".to_string(),
749 user_id: "user-delete".to_string(),
750 created_at: None,
751 };
752 storage
753 .save("session", "session-delete", session)
754 .await
755 .unwrap();
756
757 assert!(storage.exists("session-delete").await.unwrap());
759
760 storage.delete("session-delete").await.unwrap();
762
763 assert!(!storage.exists("session-delete").await.unwrap());
765 }
766
767 #[tokio::test]
768 async fn test_async_dir_storage_filename_encoding_url_roundtrip() {
769 let temp_dir = TempDir::new().unwrap();
770 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
771 temp_dir.path().to_path_buf(),
772 ));
773
774 let migrator = setup_session_migrator();
775 let strategy =
776 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::UrlEncode);
777 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
778 .await
779 .unwrap();
780
781 let complex_id = "user@example.com/path?query=1";
783 let session = SessionEntity {
784 id: complex_id.to_string(),
785 user_id: "user-special".to_string(),
786 created_at: Some("2024-05-01".to_string()),
787 };
788
789 storage
791 .save("session", complex_id, session.clone())
792 .await
793 .unwrap();
794
795 let encoded_id = urlencoding::encode(complex_id);
797 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
798 assert!(file_path.exists());
799
800 let loaded: SessionEntity = storage.load("session", complex_id).await.unwrap();
802 assert_eq!(loaded.id, session.id);
803 assert_eq!(loaded.user_id, session.user_id);
804 assert_eq!(loaded.created_at, session.created_at);
805
806 let ids = storage.list_ids().await.unwrap();
808 assert_eq!(ids.len(), 1);
809 assert_eq!(ids[0], complex_id);
810 }
811
812 #[tokio::test]
813 async fn test_async_dir_storage_filename_encoding_base64_roundtrip() {
814 let temp_dir = TempDir::new().unwrap();
815 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
816 temp_dir.path().to_path_buf(),
817 ));
818
819 let migrator = setup_session_migrator();
820 let strategy =
821 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::Base64);
822 let storage = AsyncDirStorage::new(paths, "sessions", migrator, strategy)
823 .await
824 .unwrap();
825
826 let complex_id = "user@example.com/path?query=1&special=!@#$%";
828 let session = SessionEntity {
829 id: complex_id.to_string(),
830 user_id: "user-base64".to_string(),
831 created_at: Some("2024-06-01".to_string()),
832 };
833
834 storage
836 .save("session", complex_id, session.clone())
837 .await
838 .unwrap();
839
840 let encoded_id = URL_SAFE_NO_PAD.encode(complex_id.as_bytes());
842 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
843 assert!(file_path.exists());
844
845 let loaded: SessionEntity = storage.load("session", complex_id).await.unwrap();
847 assert_eq!(loaded.id, session.id);
848 assert_eq!(loaded.user_id, session.user_id);
849 assert_eq!(loaded.created_at, session.created_at);
850
851 let ids = storage.list_ids().await.unwrap();
853 assert_eq!(ids.len(), 1);
854 assert_eq!(ids[0], complex_id);
855 }
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
863 use base64::Engine;
864 use local_store::StoreError;
865 use std::fs;
866 use tempfile::TempDir;
867
868 #[test]
869 fn test_filename_encoding_default() {
870 assert_eq!(FilenameEncoding::default(), FilenameEncoding::Direct);
871 }
872
873 #[test]
874 fn test_dir_storage_strategy_default() {
875 let strategy = DirStorageStrategy::default();
876 assert_eq!(strategy.format, FormatStrategy::Json);
877 assert_eq!(strategy.extension, None);
878 assert_eq!(strategy.filename_encoding, FilenameEncoding::Direct);
879 }
880
881 #[test]
882 fn test_dir_storage_strategy_builder() {
883 let strategy = DirStorageStrategy::new()
884 .with_format(FormatStrategy::Toml)
885 .with_extension("data")
886 .with_filename_encoding(FilenameEncoding::Base64)
887 .with_retry_count(5)
888 .with_cleanup(false);
889
890 assert_eq!(strategy.format, FormatStrategy::Toml);
891 assert_eq!(strategy.extension, Some("data".to_string()));
892 assert_eq!(strategy.filename_encoding, FilenameEncoding::Base64);
893 assert_eq!(strategy.atomic_write.retry_count, 5);
894 assert!(!strategy.atomic_write.cleanup_tmp_files);
895 }
896
897 #[test]
898 fn test_dir_storage_strategy_get_extension() {
899 let strategy1 = DirStorageStrategy::default();
901 assert_eq!(strategy1.get_extension(), "json");
902
903 let strategy2 = DirStorageStrategy::default().with_format(FormatStrategy::Toml);
905 assert_eq!(strategy2.get_extension(), "toml");
906
907 let strategy3 = DirStorageStrategy::default().with_extension("custom");
909 assert_eq!(strategy3.get_extension(), "custom");
910 }
911
912 #[test]
913 fn test_dir_storage_new_creates_directory() {
914 let temp_dir = TempDir::new().unwrap();
915 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
916 temp_dir.path().to_path_buf(),
917 ));
918
919 let migrator = Migrator::new();
920 let strategy = DirStorageStrategy::default();
921
922 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
923
924 assert!(storage.base_path().exists());
926 assert!(storage.base_path().is_dir());
927 assert!(storage.base_path().ends_with("data/testapp/sessions"));
928 }
929
930 #[test]
931 fn test_dir_storage_new_idempotent() {
932 let temp_dir = TempDir::new().unwrap();
933 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
934 temp_dir.path().to_path_buf(),
935 ));
936
937 let migrator1 = Migrator::new();
938 let migrator2 = Migrator::new();
939 let strategy = DirStorageStrategy::default();
940
941 let storage1 =
943 DirStorage::new(paths.clone(), "sessions", migrator1, strategy.clone()).unwrap();
944 let storage2 = DirStorage::new(paths, "sessions", migrator2, strategy).unwrap();
945
946 assert_eq!(storage1.base_path(), storage2.base_path());
948 }
949
950 use crate::{FromDomain, IntoDomain, MigratesTo, Versioned};
952 use serde::{Deserialize, Serialize};
953
954 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
955 struct SessionV1_0_0 {
956 id: String,
957 user_id: String,
958 }
959
960 impl Versioned for SessionV1_0_0 {
961 const VERSION: &'static str = "1.0.0";
962 }
963
964 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
965 struct SessionV1_1_0 {
966 id: String,
967 user_id: String,
968 created_at: Option<String>,
969 }
970
971 impl Versioned for SessionV1_1_0 {
972 const VERSION: &'static str = "1.1.0";
973 }
974
975 impl MigratesTo<SessionV1_1_0> for SessionV1_0_0 {
976 fn migrate(self) -> SessionV1_1_0 {
977 SessionV1_1_0 {
978 id: self.id,
979 user_id: self.user_id,
980 created_at: None,
981 }
982 }
983 }
984
985 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
986 struct SessionEntity {
987 id: String,
988 user_id: String,
989 created_at: Option<String>,
990 }
991
992 impl IntoDomain<SessionEntity> for SessionV1_1_0 {
993 fn into_domain(self) -> SessionEntity {
994 SessionEntity {
995 id: self.id,
996 user_id: self.user_id,
997 created_at: self.created_at,
998 }
999 }
1000 }
1001
1002 impl FromDomain<SessionEntity> for SessionV1_1_0 {
1003 fn from_domain(domain: SessionEntity) -> Self {
1004 SessionV1_1_0 {
1005 id: domain.id,
1006 user_id: domain.user_id,
1007 created_at: domain.created_at,
1008 }
1009 }
1010 }
1011
1012 fn setup_session_migrator() -> Migrator {
1013 let path = Migrator::define("session")
1014 .from::<SessionV1_0_0>()
1015 .step::<SessionV1_1_0>()
1016 .into_with_save::<SessionEntity>();
1017
1018 let mut migrator = Migrator::new();
1019 migrator.register(path).unwrap();
1020 migrator
1021 }
1022
1023 #[test]
1024 fn test_dir_storage_save_json() {
1025 let temp_dir = TempDir::new().unwrap();
1026 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1027 temp_dir.path().to_path_buf(),
1028 ));
1029
1030 let migrator = setup_session_migrator();
1031 let strategy = DirStorageStrategy::default().with_format(FormatStrategy::Json);
1032 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1033
1034 let session = SessionEntity {
1036 id: "session-123".to_string(),
1037 user_id: "user-456".to_string(),
1038 created_at: Some("2024-01-01T00:00:00Z".to_string()),
1039 };
1040
1041 storage.save("session", "session-123", session).unwrap();
1043
1044 let file_path = storage.base_path().join("session-123.json");
1046 assert!(file_path.exists());
1047
1048 let content = std::fs::read_to_string(&file_path).unwrap();
1050 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
1051 assert_eq!(json["version"], "1.1.0");
1052 assert_eq!(json["id"], "session-123");
1053 assert_eq!(json["user_id"], "user-456");
1054 }
1055
1056 #[test]
1057 fn test_dir_storage_save_toml() {
1058 let temp_dir = TempDir::new().unwrap();
1059 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1060 temp_dir.path().to_path_buf(),
1061 ));
1062
1063 let migrator = setup_session_migrator();
1064 let strategy = DirStorageStrategy::default().with_format(FormatStrategy::Toml);
1065 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1066
1067 let session = SessionEntity {
1069 id: "session-789".to_string(),
1070 user_id: "user-101".to_string(),
1071 created_at: Some("2024-01-15T10:30:00Z".to_string()),
1072 };
1073
1074 storage.save("session", "session-789", session).unwrap();
1076
1077 let file_path = storage.base_path().join("session-789.toml");
1079 assert!(file_path.exists());
1080
1081 let content = std::fs::read_to_string(&file_path).unwrap();
1083 let toml: toml::Value = toml::from_str(&content).unwrap();
1084 assert_eq!(toml["version"].as_str().unwrap(), "1.1.0");
1085 assert_eq!(toml["id"].as_str().unwrap(), "session-789");
1086 assert_eq!(toml["created_at"].as_str().unwrap(), "2024-01-15T10:30:00Z");
1087 }
1088
1089 #[test]
1090 fn test_dir_storage_save_with_invalid_id() {
1091 let temp_dir = TempDir::new().unwrap();
1092 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1093 temp_dir.path().to_path_buf(),
1094 ));
1095
1096 let migrator = setup_session_migrator();
1097 let strategy = DirStorageStrategy::default();
1098 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1099
1100 let session = SessionEntity {
1101 id: "invalid/id".to_string(),
1102 user_id: "user-456".to_string(),
1103 created_at: None,
1104 };
1105
1106 let result = storage.save("session", "invalid/id", session);
1108 assert!(result.is_err());
1109 assert!(matches!(
1110 result.unwrap_err(),
1111 crate::MigrationError::FilenameEncoding { .. }
1112 ));
1113 }
1114
1115 #[test]
1116 fn test_dir_storage_save_with_custom_extension() {
1117 let temp_dir = TempDir::new().unwrap();
1118 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1119 temp_dir.path().to_path_buf(),
1120 ));
1121
1122 let migrator = setup_session_migrator();
1123 let strategy = DirStorageStrategy::default()
1124 .with_format(FormatStrategy::Json)
1125 .with_extension("data");
1126 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1127
1128 let session = SessionEntity {
1129 id: "session-custom".to_string(),
1130 user_id: "user-999".to_string(),
1131 created_at: None,
1132 };
1133
1134 storage.save("session", "session-custom", session).unwrap();
1135
1136 let file_path = storage.base_path().join("session-custom.data");
1138 assert!(file_path.exists());
1139 }
1140
1141 #[test]
1142 fn test_dir_storage_save_overwrites_existing() {
1143 let temp_dir = TempDir::new().unwrap();
1144 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1145 temp_dir.path().to_path_buf(),
1146 ));
1147
1148 let migrator = setup_session_migrator();
1149 let strategy = DirStorageStrategy::default();
1150 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1151
1152 let session1 = SessionEntity {
1154 id: "session-overwrite".to_string(),
1155 user_id: "user-111".to_string(),
1156 created_at: Some("2024-01-01".to_string()),
1157 };
1158 storage
1159 .save("session", "session-overwrite", session1)
1160 .unwrap();
1161
1162 let session2 = SessionEntity {
1164 id: "session-overwrite".to_string(),
1165 user_id: "user-222".to_string(),
1166 created_at: Some("2024-01-02".to_string()),
1167 };
1168 storage
1169 .save("session", "session-overwrite", session2)
1170 .unwrap();
1171
1172 let file_path = storage.base_path().join("session-overwrite.json");
1174 let content = std::fs::read_to_string(&file_path).unwrap();
1175 let json: serde_json::Value = serde_json::from_str(&content).unwrap();
1176 assert_eq!(json["user_id"], "user-222");
1177 assert_eq!(json["created_at"], "2024-01-02");
1178 }
1179
1180 #[test]
1181 fn test_dir_storage_load_success() {
1182 let temp_dir = TempDir::new().unwrap();
1183 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1184 temp_dir.path().to_path_buf(),
1185 ));
1186
1187 let migrator = setup_session_migrator();
1188 let strategy = DirStorageStrategy::default();
1189 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1190
1191 let session = SessionEntity {
1193 id: "session-load".to_string(),
1194 user_id: "user-999".to_string(),
1195 created_at: Some("2024-02-01".to_string()),
1196 };
1197 storage
1198 .save("session", "session-load", session.clone())
1199 .unwrap();
1200
1201 let loaded: SessionEntity = storage.load("session", "session-load").unwrap();
1203 assert_eq!(loaded.id, session.id);
1204 assert_eq!(loaded.user_id, session.user_id);
1205 assert_eq!(loaded.created_at, session.created_at);
1206 }
1207
1208 #[test]
1209 fn test_dir_storage_load_not_found() {
1210 let temp_dir = TempDir::new().unwrap();
1211 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1212 temp_dir.path().to_path_buf(),
1213 ));
1214
1215 let migrator = setup_session_migrator();
1216 let strategy = DirStorageStrategy::default();
1217 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1218
1219 let result: Result<SessionEntity, _> = storage.load("session", "non-existent");
1221 assert!(result.is_err());
1222 assert!(matches!(
1223 result.unwrap_err(),
1224 MigrationError::Store(StoreError::IoError { .. })
1225 ));
1226 }
1227
1228 #[test]
1229 fn test_dir_storage_save_and_load_roundtrip() {
1230 let temp_dir = TempDir::new().unwrap();
1231 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1232 temp_dir.path().to_path_buf(),
1233 ));
1234
1235 let migrator = setup_session_migrator();
1236 let strategy = DirStorageStrategy::default();
1237 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1238
1239 let sessions = vec![
1241 SessionEntity {
1242 id: "session-1".to_string(),
1243 user_id: "user-1".to_string(),
1244 created_at: Some("2024-01-01".to_string()),
1245 },
1246 SessionEntity {
1247 id: "session-2".to_string(),
1248 user_id: "user-2".to_string(),
1249 created_at: None,
1250 },
1251 SessionEntity {
1252 id: "session-3".to_string(),
1253 user_id: "user-3".to_string(),
1254 created_at: Some("2024-03-01".to_string()),
1255 },
1256 ];
1257
1258 for session in &sessions {
1260 storage
1261 .save("session", &session.id, session.clone())
1262 .unwrap();
1263 }
1264
1265 for session in &sessions {
1267 let loaded: SessionEntity = storage.load("session", &session.id).unwrap();
1268 assert_eq!(loaded.id, session.id);
1269 assert_eq!(loaded.user_id, session.user_id);
1270 assert_eq!(loaded.created_at, session.created_at);
1271 }
1272 }
1273
1274 #[test]
1275 fn test_dir_storage_list_ids_empty() {
1276 let temp_dir = TempDir::new().unwrap();
1277 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1278 temp_dir.path().to_path_buf(),
1279 ));
1280
1281 let migrator = setup_session_migrator();
1282 let strategy = DirStorageStrategy::default();
1283 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1284
1285 let ids = storage.list_ids().unwrap();
1287 assert!(ids.is_empty());
1288 }
1289
1290 #[test]
1291 fn test_dir_storage_list_ids() {
1292 let temp_dir = TempDir::new().unwrap();
1293 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1294 temp_dir.path().to_path_buf(),
1295 ));
1296
1297 let migrator = setup_session_migrator();
1298 let strategy = DirStorageStrategy::default();
1299 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1300
1301 let ids = vec!["session-c", "session-a", "session-b"];
1303 for id in &ids {
1304 let session = SessionEntity {
1305 id: id.to_string(),
1306 user_id: "user".to_string(),
1307 created_at: None,
1308 };
1309 storage.save("session", id, session).unwrap();
1310 }
1311
1312 let listed_ids = storage.list_ids().unwrap();
1314 assert_eq!(listed_ids.len(), 3);
1315 assert_eq!(listed_ids, vec!["session-a", "session-b", "session-c"]);
1317 }
1318
1319 #[test]
1320 fn test_dir_storage_load_all_empty() {
1321 let temp_dir = TempDir::new().unwrap();
1322 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1323 temp_dir.path().to_path_buf(),
1324 ));
1325
1326 let migrator = setup_session_migrator();
1327 let strategy = DirStorageStrategy::default();
1328 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1329
1330 let results: Vec<(String, SessionEntity)> = storage.load_all("session").unwrap();
1332 assert!(results.is_empty());
1333 }
1334
1335 #[test]
1336 fn test_dir_storage_load_all() {
1337 let temp_dir = TempDir::new().unwrap();
1338 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1339 temp_dir.path().to_path_buf(),
1340 ));
1341
1342 let migrator = setup_session_migrator();
1343 let strategy = DirStorageStrategy::default();
1344 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1345
1346 let sessions = vec![
1348 SessionEntity {
1349 id: "session-x".to_string(),
1350 user_id: "user-x".to_string(),
1351 created_at: Some("2024-01-01".to_string()),
1352 },
1353 SessionEntity {
1354 id: "session-y".to_string(),
1355 user_id: "user-y".to_string(),
1356 created_at: None,
1357 },
1358 SessionEntity {
1359 id: "session-z".to_string(),
1360 user_id: "user-z".to_string(),
1361 created_at: Some("2024-03-01".to_string()),
1362 },
1363 ];
1364
1365 for session in &sessions {
1366 storage
1367 .save("session", &session.id, session.clone())
1368 .unwrap();
1369 }
1370
1371 let results: Vec<(String, SessionEntity)> = storage.load_all("session").unwrap();
1373 assert_eq!(results.len(), 3);
1374
1375 for (id, loaded) in &results {
1377 let original = sessions.iter().find(|s| &s.id == id).unwrap();
1378 assert_eq!(loaded.id, original.id);
1379 assert_eq!(loaded.user_id, original.user_id);
1380 assert_eq!(loaded.created_at, original.created_at);
1381 }
1382 }
1383
1384 #[test]
1385 fn test_dir_storage_exists() {
1386 let temp_dir = TempDir::new().unwrap();
1387 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1388 temp_dir.path().to_path_buf(),
1389 ));
1390
1391 let migrator = setup_session_migrator();
1392 let strategy = DirStorageStrategy::default();
1393 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1394
1395 assert!(!storage.exists("session-exists").unwrap());
1397
1398 let session = SessionEntity {
1400 id: "session-exists".to_string(),
1401 user_id: "user-exists".to_string(),
1402 created_at: None,
1403 };
1404 storage.save("session", "session-exists", session).unwrap();
1405
1406 assert!(storage.exists("session-exists").unwrap());
1408 }
1409
1410 #[test]
1411 fn test_dir_storage_delete() {
1412 let temp_dir = TempDir::new().unwrap();
1413 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1414 temp_dir.path().to_path_buf(),
1415 ));
1416
1417 let migrator = setup_session_migrator();
1418 let strategy = DirStorageStrategy::default();
1419 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1420
1421 let session = SessionEntity {
1423 id: "session-delete".to_string(),
1424 user_id: "user-delete".to_string(),
1425 created_at: None,
1426 };
1427 storage.save("session", "session-delete", session).unwrap();
1428
1429 assert!(storage.exists("session-delete").unwrap());
1431
1432 storage.delete("session-delete").unwrap();
1434
1435 assert!(!storage.exists("session-delete").unwrap());
1437 }
1438
1439 #[test]
1440 fn test_dir_storage_delete_idempotent() {
1441 let temp_dir = TempDir::new().unwrap();
1442 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1443 temp_dir.path().to_path_buf(),
1444 ));
1445
1446 let migrator = setup_session_migrator();
1447 let strategy = DirStorageStrategy::default();
1448 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1449
1450 storage.delete("non-existent").unwrap();
1452
1453 storage.delete("non-existent").unwrap();
1455 }
1456
1457 #[test]
1458 fn test_dir_storage_load_toml() {
1459 let temp_dir = TempDir::new().unwrap();
1460 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1461 temp_dir.path().to_path_buf(),
1462 ));
1463
1464 let migrator = setup_session_migrator();
1465 let strategy = DirStorageStrategy::default().with_format(FormatStrategy::Toml);
1466 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1467
1468 let session = SessionEntity {
1470 id: "session-toml".to_string(),
1471 user_id: "user-toml".to_string(),
1472 created_at: Some("2024-04-01".to_string()),
1473 };
1474 storage
1475 .save("session", "session-toml", session.clone())
1476 .unwrap();
1477
1478 let loaded: SessionEntity = storage.load("session", "session-toml").unwrap();
1480 assert_eq!(loaded.id, session.id);
1481 assert_eq!(loaded.user_id, session.user_id);
1482 assert_eq!(loaded.created_at, session.created_at);
1483 }
1484
1485 #[test]
1486 fn test_dir_storage_list_ids_with_custom_extension() {
1487 let temp_dir = TempDir::new().unwrap();
1488 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1489 temp_dir.path().to_path_buf(),
1490 ));
1491
1492 let migrator = setup_session_migrator();
1493 let strategy = DirStorageStrategy::default().with_extension("data");
1494 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1495
1496 let session = SessionEntity {
1498 id: "session-ext".to_string(),
1499 user_id: "user-ext".to_string(),
1500 created_at: None,
1501 };
1502 storage.save("session", "session-ext", session).unwrap();
1503
1504 let ids = storage.list_ids().unwrap();
1506 assert_eq!(ids.len(), 1);
1507 assert_eq!(ids[0], "session-ext");
1508 }
1509
1510 #[test]
1511 fn test_dir_storage_load_all_atomic_failure() {
1512 let temp_dir = TempDir::new().unwrap();
1513 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1514 temp_dir.path().to_path_buf(),
1515 ));
1516
1517 let migrator = setup_session_migrator();
1518 let strategy = DirStorageStrategy::default();
1519 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1520
1521 let session1 = SessionEntity {
1523 id: "session-1".to_string(),
1524 user_id: "user-1".to_string(),
1525 created_at: None,
1526 };
1527 storage.save("session", "session-1", session1).unwrap();
1528
1529 let corrupted_path = storage.base_path().join("session-corrupted.json");
1531 std::fs::write(&corrupted_path, "invalid json {{{").unwrap();
1532
1533 let result: Result<Vec<(String, SessionEntity)>, _> = storage.load_all("session");
1535 assert!(result.is_err());
1536 }
1537
1538 #[test]
1539 fn test_dir_storage_filename_encoding_url_roundtrip() {
1540 let temp_dir = TempDir::new().unwrap();
1541 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1542 temp_dir.path().to_path_buf(),
1543 ));
1544
1545 let migrator = setup_session_migrator();
1546 let strategy =
1547 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::UrlEncode);
1548 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1549
1550 let complex_id = "user@example.com/path?query=1";
1552 let session = SessionEntity {
1553 id: complex_id.to_string(),
1554 user_id: "user-special".to_string(),
1555 created_at: Some("2024-05-01".to_string()),
1556 };
1557
1558 storage
1560 .save("session", complex_id, session.clone())
1561 .unwrap();
1562
1563 let encoded_id = urlencoding::encode(complex_id);
1565 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
1566 assert!(file_path.exists());
1567
1568 let loaded: SessionEntity = storage.load("session", complex_id).unwrap();
1570 assert_eq!(loaded.id, session.id);
1571 assert_eq!(loaded.user_id, session.user_id);
1572 assert_eq!(loaded.created_at, session.created_at);
1573
1574 let ids = storage.list_ids().unwrap();
1576 assert_eq!(ids.len(), 1);
1577 assert_eq!(ids[0], complex_id);
1578 }
1579
1580 #[test]
1581 fn test_dir_storage_filename_encoding_base64_roundtrip() {
1582 let temp_dir = TempDir::new().unwrap();
1583 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1584 temp_dir.path().to_path_buf(),
1585 ));
1586
1587 let migrator = setup_session_migrator();
1588 let strategy =
1589 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::Base64);
1590 let storage = DirStorage::new(paths, "sessions", migrator, strategy).unwrap();
1591
1592 let complex_id = "user@example.com/path?query=1&special=!@#$%";
1594 let session = SessionEntity {
1595 id: complex_id.to_string(),
1596 user_id: "user-base64".to_string(),
1597 created_at: Some("2024-06-01".to_string()),
1598 };
1599
1600 storage
1602 .save("session", complex_id, session.clone())
1603 .unwrap();
1604
1605 let encoded_id = URL_SAFE_NO_PAD.encode(complex_id.as_bytes());
1607 let file_path = storage.base_path().join(format!("{}.json", encoded_id));
1608 assert!(file_path.exists());
1609
1610 let loaded: SessionEntity = storage.load("session", complex_id).unwrap();
1612 assert_eq!(loaded.id, session.id);
1613 assert_eq!(loaded.user_id, session.user_id);
1614 assert_eq!(loaded.created_at, session.created_at);
1615
1616 let ids = storage.list_ids().unwrap();
1618 assert_eq!(ids.len(), 1);
1619 assert_eq!(ids[0], complex_id);
1620 }
1621
1622 #[test]
1626 fn test_list_ids_url_decode_error() {
1627 let temp_dir = TempDir::new().unwrap();
1628 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1629 temp_dir.path().to_path_buf(),
1630 ));
1631
1632 let migrator = setup_session_migrator();
1633 let strategy =
1634 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::UrlEncode);
1635 let storage = DirStorage::new(paths, "sessions_url_err", migrator, strategy).unwrap();
1636
1637 let bad_stem = "%C0%C1";
1640 let bad_file = storage.base_path().join(format!("{}.json", bad_stem));
1641 std::fs::write(&bad_file, "{}").unwrap();
1642
1643 let result = storage.list_ids();
1644 assert!(
1645 result.is_err(),
1646 "list_ids should propagate the FilenameEncoding decode error"
1647 );
1648 assert!(matches!(
1649 result.unwrap_err(),
1650 MigrationError::FilenameEncoding { .. }
1651 ));
1652 }
1653
1654 #[test]
1657 fn test_list_ids_base64_decode_error() {
1658 let temp_dir = TempDir::new().unwrap();
1659 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1660 temp_dir.path().to_path_buf(),
1661 ));
1662
1663 let migrator = setup_session_migrator();
1664 let strategy =
1665 DirStorageStrategy::default().with_filename_encoding(FilenameEncoding::Base64);
1666 let storage = DirStorage::new(paths, "sessions_b64_err", migrator, strategy).unwrap();
1667
1668 let bad_stem = "!!!invalid@@@";
1670 let bad_file = storage.base_path().join(format!("{}.json", bad_stem));
1671 std::fs::write(&bad_file, "{}").unwrap();
1672
1673 let result = storage.list_ids();
1674 assert!(
1675 result.is_err(),
1676 "list_ids should propagate the FilenameEncoding decode error"
1677 );
1678 assert!(matches!(
1679 result.unwrap_err(),
1680 MigrationError::FilenameEncoding { .. }
1681 ));
1682 }
1683
1684 #[test]
1685 fn test_dir_storage_base_path() {
1686 let temp_dir = TempDir::new().unwrap();
1687 let domain_name = "test_sessions";
1688 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1689 temp_dir.path().to_path_buf(),
1690 ));
1691
1692 let migrator = Migrator::new();
1693 let strategy = DirStorageStrategy::default();
1694
1695 let storage = DirStorage::new(paths, domain_name, migrator, strategy).unwrap();
1696
1697 let returned_path = storage.base_path();
1699 assert!(returned_path.ends_with(domain_name));
1700 assert!(returned_path.exists());
1701 }
1702
1703 #[test]
1704 #[cfg(unix)]
1705 fn test_dir_storage_create_dir_permission_denied() {
1706 use std::os::unix::fs::PermissionsExt;
1707
1708 let temp_dir = TempDir::new().unwrap();
1709
1710 let mut perms = fs::metadata(temp_dir.path()).unwrap().permissions();
1712 perms.set_mode(0o444);
1713 fs::set_permissions(temp_dir.path(), perms).unwrap();
1714
1715 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1716 temp_dir.path().to_path_buf(),
1717 ));
1718
1719 let migrator = Migrator::new();
1720 let strategy = DirStorageStrategy::default();
1721
1722 let result = DirStorage::new(paths, "sessions", migrator, strategy);
1724
1725 let mut perms = fs::metadata(temp_dir.path()).unwrap().permissions();
1727 perms.set_mode(0o755);
1728 fs::set_permissions(temp_dir.path(), perms).unwrap();
1729
1730 assert!(result.is_err());
1731 assert!(matches!(
1732 result,
1733 Err(MigrationError::Store(StoreError::IoError { .. }))
1734 ));
1735 }
1736
1737 #[test]
1738 #[cfg(unix)]
1739 fn test_dir_storage_save_permission_denied() {
1740 use std::os::unix::fs::PermissionsExt;
1741
1742 let temp_dir = TempDir::new().unwrap();
1743 let domain_name = "sessions_readonly";
1744 let paths = AppPaths::new("testapp").data_strategy(crate::PathStrategy::CustomBase(
1745 temp_dir.path().to_path_buf(),
1746 ));
1747
1748 let migrator = setup_session_migrator();
1749 let strategy = DirStorageStrategy::default();
1750
1751 let storage = DirStorage::new(paths, domain_name, migrator, strategy).unwrap();
1753
1754 let mut perms = fs::metadata(storage.base_path()).unwrap().permissions();
1756 perms.set_mode(0o444);
1757 fs::set_permissions(storage.base_path(), perms).unwrap();
1758
1759 let session = SessionEntity {
1760 id: "test".to_string(),
1761 user_id: "user".to_string(),
1762 created_at: None,
1763 };
1764
1765 let result = storage.save("session", "test-session", session);
1767
1768 let mut perms = fs::metadata(storage.base_path()).unwrap().permissions();
1770 perms.set_mode(0o755);
1771 fs::set_permissions(storage.base_path(), perms).unwrap();
1772
1773 assert!(result.is_err());
1774 assert!(matches!(
1775 result,
1776 Err(MigrationError::Store(StoreError::IoError { .. }))
1777 ));
1778 }
1779}