use std::{collections::HashMap, sync::Arc}; use offline_web_model::Reference; use offline_web_storage::ReferenceStore; async fn create_test_store() -> ReferenceStore { let store = ReferenceStore::new("sqlite::memory:").await.unwrap(); store } #[tokio::test] async fn test_store_and_retrieve_reference() { let store = create_test_store().await; let reference = Reference::new(Some("abc123".to_string()), "test.txt".to_string()); // Store the reference store.store_reference(&reference).await.unwrap(); // Retrieve it let retrieved = store.get_reference(&reference.id).await.unwrap(); assert!(retrieved.is_some()); let retrieved = retrieved.unwrap(); assert_eq!(retrieved.id, reference.id); assert_eq!(retrieved.content_address, reference.content_address); assert_eq!(retrieved.name, reference.name); assert_eq!(retrieved.dependents.len(), 0); } #[tokio::test] async fn test_store_reference_with_dependents() { let store = create_test_store().await; let dep1 = Arc::new(Reference::new( Some("dep1".to_string()), "dep1.txt".to_string(), )); let dep2 = Arc::new(Reference::new( Some("dep2".to_string()), "dep2.txt".to_string(), )); // Store dependencies first store.store_reference(&dep1).await.unwrap(); store.store_reference(&dep2).await.unwrap(); let mut parent = Reference::new(Some("parent".to_string()), "parent.txt".to_string()); parent = parent.add_dep(dep1.clone()); parent = parent.add_dep(dep2.clone()); // Store parent with dependencies store.store_reference(&parent).await.unwrap(); // Retrieve and verify let retrieved = store.get_reference(&parent.id).await.unwrap().unwrap(); assert_eq!(retrieved.dependents.len(), 2); let dep_names: Vec<_> = retrieved.dependents.iter().map(|d| &d.name).collect(); assert!(dep_names.contains(&&"dep1.txt".to_string())); assert!(dep_names.contains(&&"dep2.txt".to_string())); } #[tokio::test] async fn test_get_references_by_name() { let store = create_test_store().await; let ref1 = Reference::new(Some("abc1".to_string()), "test.txt".to_string()); let ref2 = Reference::new(Some("abc2".to_string()), "test.txt".to_string()); let ref3 = Reference::new(Some("abc3".to_string()), "other.txt".to_string()); store.store_reference(&ref1).await.unwrap(); store.store_reference(&ref2).await.unwrap(); store.store_reference(&ref3).await.unwrap(); let results = store.get_references_by_name("test.txt").await.unwrap(); assert_eq!(results.len(), 2); let results = store.get_references_by_name("other.txt").await.unwrap(); assert_eq!(results.len(), 1); let results = store .get_references_by_name("nonexistent.txt") .await .unwrap(); assert_eq!(results.len(), 0); } #[tokio::test] async fn test_get_references_by_content_address() { let store = create_test_store().await; let ref1 = Reference::new(Some("same_content".to_string()), "file1.txt".to_string()); let ref2 = Reference::new(Some("same_content".to_string()), "file2.txt".to_string()); let ref3 = Reference::new( Some("different_content".to_string()), "file3.txt".to_string(), ); store.store_reference(&ref1).await.unwrap(); store.store_reference(&ref2).await.unwrap(); store.store_reference(&ref3).await.unwrap(); let results = store .get_references_by_content_address("same_content") .await .unwrap(); assert_eq!(results.len(), 2); let results = store .get_references_by_content_address("different_content") .await .unwrap(); assert_eq!(results.len(), 1); } #[tokio::test] async fn test_delete_reference() { let store = create_test_store().await; let reference = Reference::new(Some("abc123".to_string()), "test.txt".to_string()); store.store_reference(&reference).await.unwrap(); // Verify it exists let retrieved = store.get_reference(&reference.id).await.unwrap(); assert!(retrieved.is_some()); // Delete it let deleted = store.delete_reference(&reference.id).await.unwrap(); assert!(deleted); // Verify it's gone let retrieved = store.get_reference(&reference.id).await.unwrap(); assert!(retrieved.is_none()); // Try to delete again let deleted = store.delete_reference(&reference.id).await.unwrap(); assert!(!deleted); } #[tokio::test] async fn test_list_all_references() { let store = create_test_store().await; let ref1 = Reference::new(Some("abc1".to_string()), "a.txt".to_string()); let ref2 = Reference::new(Some("abc2".to_string()), "b.txt".to_string()); let ref3 = Reference::new(Some("abc3".to_string()), "c.txt".to_string()); store.store_reference(&ref1).await.unwrap(); store.store_reference(&ref2).await.unwrap(); store.store_reference(&ref3).await.unwrap(); let all_refs = store.list_all_references().await.unwrap(); assert_eq!(all_refs.len(), 3); // Should be sorted by name assert_eq!(all_refs[0].name, "a.txt"); assert_eq!(all_refs[1].name, "b.txt"); assert_eq!(all_refs[2].name, "c.txt"); } #[tokio::test] async fn test_update_reference_graph() { let store = create_test_store().await; let ref1 = Arc::new(Reference::new( Some("abc1".to_string()), "file1.txt".to_string(), )); let ref2 = Arc::new(Reference::new( Some("abc2".to_string()), "file2.txt".to_string(), )); let mut updated_refs = HashMap::new(); updated_refs.insert(ref1.id.clone(), ref1.clone()); updated_refs.insert(ref2.id.clone(), ref2.clone()); store.update_reference_graph(&updated_refs).await.unwrap(); // Verify both references were stored let retrieved1 = store.get_reference(&ref1.id).await.unwrap(); let retrieved2 = store.get_reference(&ref2.id).await.unwrap(); assert!(retrieved1.is_some()); assert!(retrieved2.is_some()); assert_eq!(retrieved1.unwrap().name, "file1.txt"); assert_eq!(retrieved2.unwrap().name, "file2.txt"); } #[tokio::test] async fn test_store_and_retrieve_content() { let store = create_test_store().await; let content = b"Hello, world!"; let content_type = Some("text/plain".to_string()); // Store content let content_address = store .store_content(content, content_type.clone()) .await .unwrap(); // Retrieve content let retrieved_content = store.get_content(&content_address).await.unwrap(); assert!(retrieved_content.is_some()); assert_eq!(retrieved_content.unwrap(), content); // Check content info let content_info = store.get_content_info(&content_address).await.unwrap(); assert!(content_info.is_some()); let info = content_info.unwrap(); assert_eq!(info.content_address, content_address); assert_eq!(info.content_type, content_type); // Check content exists assert!(store.content_exists(&content_address).await.unwrap()); } #[tokio::test] async fn test_content_deduplication() { let store = create_test_store().await; let content = b"Duplicate content"; // Store same content twice let addr1 = store.store_content(content, None).await.unwrap(); let addr2 = store.store_content(content, None).await.unwrap(); // Should get same address assert_eq!(addr1, addr2); // Should only have one copy in storage let stats = store.get_storage_stats().await.unwrap(); assert_eq!(stats.content_object_count, 1); } #[tokio::test] async fn test_store_reference_with_content() { let store = create_test_store().await; let content = b"File content here"; let name = "test_file.txt".to_string(); let content_type = Some("text/plain".to_string()); // Store reference with content let reference = store .store_reference_with_content(name.clone(), content, content_type) .await .unwrap(); assert_eq!(reference.name, name); assert!(reference.content_address.is_some()); // Retrieve reference with content let (retrieved_ref, retrieved_content) = store .get_reference_with_content(&reference.id) .await .unwrap() .unwrap(); assert_eq!(retrieved_ref.id, reference.id); assert_eq!(retrieved_ref.name, name); assert!(retrieved_content.is_some()); assert_eq!(retrieved_content.unwrap(), content); } #[tokio::test] async fn test_calculate_content_address() { let content1 = b"Same content"; let content2 = b"Same content"; let content3 = b"Different content"; let addr1 = ReferenceStore::calculate_content_address(content1); let addr2 = ReferenceStore::calculate_content_address(content2); let addr3 = ReferenceStore::calculate_content_address(content3); assert_eq!(addr1, addr2); assert_ne!(addr1, addr3); // Should be a valid hex string assert!(addr1.chars().all(|c| c.is_ascii_hexdigit())); assert_eq!(addr1.len(), 128); // Blake2b512 produces 64 bytes = 128 hex chars } #[tokio::test] async fn test_delete_content() { let store = create_test_store().await; let content = b"Content to delete"; let content_address = store.store_content(content, None).await.unwrap(); // Verify content exists assert!(store.content_exists(&content_address).await.unwrap()); // Delete content let deleted = store.delete_content(&content_address).await.unwrap(); assert!(deleted); // Verify content is gone assert!(!store.content_exists(&content_address).await.unwrap()); let retrieved = store.get_content(&content_address).await.unwrap(); assert!(retrieved.is_none()); // Try to delete again let deleted_again = store.delete_content(&content_address).await.unwrap(); assert!(!deleted_again); } #[tokio::test] async fn test_cleanup_unreferenced_content() { let store = create_test_store().await; // Store some content with references let content1 = b"Referenced content"; let ref1 = store .store_reference_with_content("file1.txt".to_string(), content1, None) .await .unwrap(); // Store content without references let content2 = b"Unreferenced content 1"; let content3 = b"Unreferenced content 2"; let _addr2 = store.store_content(content2, None).await.unwrap(); let _addr3 = store.store_content(content3, None).await.unwrap(); // Initial stats let stats = store.get_storage_stats().await.unwrap(); assert_eq!(stats.content_object_count, 3); assert_eq!(stats.reference_count, 1); // List unreferenced content let unreferenced = store.list_unreferenced_content().await.unwrap(); assert_eq!(unreferenced.len(), 2); // Cleanup unreferenced content let cleaned_up = store.cleanup_unreferenced_content().await.unwrap(); assert_eq!(cleaned_up, 2); // Check final stats let final_stats = store.get_storage_stats().await.unwrap(); assert_eq!(final_stats.content_object_count, 1); assert_eq!(final_stats.reference_count, 1); // Referenced content should still exist let (retrieved_ref, retrieved_content) = store .get_reference_with_content(&ref1.id) .await .unwrap() .unwrap(); assert_eq!(retrieved_ref.id, ref1.id); assert_eq!(retrieved_content.unwrap(), content1); } #[tokio::test] async fn test_storage_stats() { let store = create_test_store().await; // Initial stats should be empty let stats = store.get_storage_stats().await.unwrap(); assert_eq!(stats.content_object_count, 0); assert_eq!(stats.total_content_size, 0); assert_eq!(stats.reference_count, 0); // Add some content and references let content1 = b"First file"; let content2 = b"Second file content"; let _ref1 = store .store_reference_with_content("file1.txt".to_string(), content1, None) .await .unwrap(); let _ref2 = store .store_reference_with_content("file2.txt".to_string(), content2, None) .await .unwrap(); // Check updated stats let final_stats = store.get_storage_stats().await.unwrap(); assert_eq!(final_stats.content_object_count, 2); assert_eq!(final_stats.reference_count, 2); } #[tokio::test] async fn test_reference_with_content_and_dependencies() { let store = create_test_store().await; // Create dependencies with content let dep1_content = b"Dependency 1 content"; let dep2_content = b"Dependency 2 content"; let dep1 = store .store_reference_with_content("dep1.txt".to_string(), dep1_content, None) .await .unwrap(); let dep2 = store .store_reference_with_content("dep2.txt".to_string(), dep2_content, None) .await .unwrap(); // Create parent with content and dependencies let parent_content = b"Parent content"; let parent = store .store_reference_with_content("parent.txt".to_string(), parent_content, None) .await .unwrap(); // Add dependencies to parent let parent_with_deps = parent.add_dep(Arc::new(dep1)).add_dep(Arc::new(dep2)); store.store_reference(&parent_with_deps).await.unwrap(); // Retrieve parent with content let (retrieved_parent, retrieved_content) = store .get_reference_with_content(&parent_with_deps.id) .await .unwrap() .unwrap(); assert_eq!(retrieved_parent.dependents.len(), 2); assert_eq!(retrieved_content.unwrap(), parent_content); // Check that dependencies also have their content for dep in &retrieved_parent.dependents { let (_, dep_content) = store .get_reference_with_content(&dep.id) .await .unwrap() .unwrap(); assert!(dep_content.is_some()); } }