use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use domain::ports::ManagedTempStore; use domain::{ ManagedTempAllocation, ManagedTempError, ManagedTempHandle, ManagedTempKind, ManagedTempState, ProjectId, }; use infrastructure::FsManagedTempStore; use uuid::Uuid; fn temp_app_data(tag: &str) -> PathBuf { let unique = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); std::env::temp_dir().join(format!( "idea-managed-temp-{tag}-{}-{unique}", std::process::id() )) } fn handle(value: &str) -> ManagedTempHandle { ManagedTempHandle::new(value).unwrap() } fn allocation( handle: ManagedTempHandle, kind: ManagedTempKind, purpose: &str, created_at_ms: u64, expires_at_ms: Option, ) -> ManagedTempAllocation { ManagedTempAllocation { handle, kind, owner_project_id: Some(ProjectId::from_uuid(Uuid::from_u128(42))), purpose: purpose.to_owned(), created_at_ms, expires_at_ms, } } fn assert_under(path: &Path, root: &Path) { assert!( path.starts_with(root), "{} should be under {}", path.display(), root.display() ); } #[tokio::test] async fn allocate_resolve_release_and_gc_directory() { let app_data = temp_app_data("dir"); let store = FsManagedTempStore::new(&app_data); let temp_root = std::env::temp_dir(); assert_eq!(store.root(), app_data.join("managed-tmp")); assert_ne!(store.root(), temp_root.as_path()); let lease = store .allocate(allocation( handle("alloc_dir_1"), ManagedTempKind::Directory, "scratch", 1_000, Some(2_000), )) .await .unwrap(); let path = PathBuf::from(lease.path.as_str()); assert_eq!(lease.state, ManagedTempState::Active); assert_eq!(lease.last_access_at_ms, 1_000); assert_under(&path, store.root()); assert!(tokio::fs::metadata(&path).await.unwrap().is_dir()); assert!( tokio::fs::metadata(store.root().join("alloc_dir_1").join("metadata.json")) .await .is_ok() ); let resolved = store.resolve(&handle("alloc_dir_1"), 1_500).await.unwrap(); assert_eq!(resolved.state, ManagedTempState::Active); assert_eq!(resolved.last_access_at_ms, 1_500); let released = store.release(&handle("alloc_dir_1"), 1_600).await.unwrap(); assert_eq!(released.state, ManagedTempState::Released); assert_eq!(released.released_at_ms, Some(1_600)); assert!(matches!( store.resolve(&handle("alloc_dir_1"), 1_700).await, Err(ManagedTempError::NotFound) )); let report = store.gc(1_700).await.unwrap(); assert_eq!(report.deleted, 1); assert!(tokio::fs::metadata(store.root().join("alloc_dir_1")) .await .is_err()); let _ = tokio::fs::remove_dir_all(app_data).await; } #[tokio::test] async fn expired_file_is_not_resolved_and_is_gc_eligible() { let app_data = temp_app_data("file"); let store = FsManagedTempStore::new(&app_data); let lease = store .allocate(allocation( handle("alloc_file_1"), ManagedTempKind::File, "../../must-not-affect-path", 1_000, Some(1_100), )) .await .unwrap(); let path = PathBuf::from(lease.path.as_str()); assert_under(&path, store.root()); assert_eq!(path.file_name().unwrap(), "data"); assert!(tokio::fs::metadata(&path).await.unwrap().is_file()); assert!(matches!( store.resolve(&handle("alloc_file_1"), 1_200).await, Err(ManagedTempError::NotFound) )); assert_eq!(store.gc(1_200).await.unwrap().deleted, 1); assert!(tokio::fs::metadata(path).await.is_err()); let _ = tokio::fs::remove_dir_all(app_data).await; } #[tokio::test] async fn invalid_path_like_handles_are_rejected_before_filesystem_resolution() { assert!(ManagedTempHandle::new("../escape").is_err()); assert!(ManagedTempHandle::new("a/b").is_err()); assert!(ManagedTempHandle::new("..\\escape").is_err()); assert!(ManagedTempHandle::new("").is_err()); } #[tokio::test] async fn handle_collision_does_not_reuse_existing_allocation() { let app_data = temp_app_data("collision"); let store = FsManagedTempStore::new(&app_data); store .allocate(allocation( handle("same_handle"), ManagedTempKind::Directory, "first", 1_000, None, )) .await .unwrap(); assert!(matches!( store .allocate(allocation( handle("same_handle"), ManagedTempKind::Directory, "second", 1_100, None, )) .await, Err(ManagedTempError::Store(_)) )); let resolved = store.resolve(&handle("same_handle"), 1_200).await.unwrap(); assert_eq!(resolved.purpose, "first"); let _ = tokio::fs::remove_dir_all(app_data).await; }