432 lines
14 KiB
Rust
432 lines
14 KiB
Rust
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());
|
|
}
|
|
}
|