initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
This commit is contained in:
commit
6a73d11c4b
124 changed files with 34856 additions and 0 deletions
414
crates/pinakes-core/tests/integration_test.rs
Normal file
414
crates/pinakes-core/tests/integration_test.rs
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use pinakes_core::model::*;
|
||||
use pinakes_core::storage::StorageBackend;
|
||||
use pinakes_core::storage::sqlite::SqliteBackend;
|
||||
|
||||
async fn setup() -> Arc<SqliteBackend> {
|
||||
let backend = SqliteBackend::in_memory().expect("in-memory SQLite");
|
||||
backend.run_migrations().await.expect("migrations");
|
||||
Arc::new(backend)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_media_crud() {
|
||||
let storage = setup().await;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let id = MediaId::new();
|
||||
let item = MediaItem {
|
||||
id,
|
||||
path: "/tmp/test.txt".into(),
|
||||
file_name: "test.txt".to_string(),
|
||||
media_type: pinakes_core::media_type::MediaType::PlainText,
|
||||
content_hash: ContentHash::new("abc123".to_string()),
|
||||
file_size: 100,
|
||||
title: Some("Test Title".to_string()),
|
||||
artist: None,
|
||||
album: None,
|
||||
genre: None,
|
||||
year: Some(2024),
|
||||
duration_secs: None,
|
||||
description: Some("A test file".to_string()),
|
||||
thumbnail_path: None,
|
||||
custom_fields: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
// Insert
|
||||
storage.insert_media(&item).await.unwrap();
|
||||
|
||||
// Get
|
||||
let fetched = storage.get_media(id).await.unwrap();
|
||||
assert_eq!(fetched.id, id);
|
||||
assert_eq!(fetched.title.as_deref(), Some("Test Title"));
|
||||
assert_eq!(fetched.file_size, 100);
|
||||
|
||||
// Get by hash
|
||||
let by_hash = storage
|
||||
.get_media_by_hash(&ContentHash::new("abc123".into()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(by_hash.is_some());
|
||||
assert_eq!(by_hash.unwrap().id, id);
|
||||
|
||||
// Update
|
||||
let mut updated = fetched;
|
||||
updated.title = Some("Updated Title".to_string());
|
||||
storage.update_media(&updated).await.unwrap();
|
||||
let re_fetched = storage.get_media(id).await.unwrap();
|
||||
assert_eq!(re_fetched.title.as_deref(), Some("Updated Title"));
|
||||
|
||||
// List
|
||||
let list = storage.list_media(&Pagination::default()).await.unwrap();
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
// Delete
|
||||
storage.delete_media(id).await.unwrap();
|
||||
let result = storage.get_media(id).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tags() {
|
||||
let storage = setup().await;
|
||||
|
||||
// Create tags
|
||||
let parent = storage.create_tag("Music", None).await.unwrap();
|
||||
let child = storage.create_tag("Rock", Some(parent.id)).await.unwrap();
|
||||
|
||||
assert_eq!(parent.name, "Music");
|
||||
assert_eq!(child.parent_id, Some(parent.id));
|
||||
|
||||
// List tags
|
||||
let tags = storage.list_tags().await.unwrap();
|
||||
assert_eq!(tags.len(), 2);
|
||||
|
||||
// Get descendants
|
||||
let descendants = storage.get_tag_descendants(parent.id).await.unwrap();
|
||||
assert!(descendants.iter().any(|t| t.name == "Rock"));
|
||||
|
||||
// Tag media
|
||||
let now = chrono::Utc::now();
|
||||
let id = MediaId::new();
|
||||
let item = MediaItem {
|
||||
id,
|
||||
path: "/tmp/song.mp3".into(),
|
||||
file_name: "song.mp3".to_string(),
|
||||
media_type: pinakes_core::media_type::MediaType::Mp3,
|
||||
content_hash: ContentHash::new("hash1".to_string()),
|
||||
file_size: 5000,
|
||||
title: Some("Test Song".to_string()),
|
||||
artist: Some("Test Artist".to_string()),
|
||||
album: None,
|
||||
genre: None,
|
||||
year: None,
|
||||
duration_secs: Some(180.0),
|
||||
description: None,
|
||||
thumbnail_path: None,
|
||||
custom_fields: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
storage.insert_media(&item).await.unwrap();
|
||||
storage.tag_media(id, parent.id).await.unwrap();
|
||||
|
||||
let media_tags = storage.get_media_tags(id).await.unwrap();
|
||||
assert_eq!(media_tags.len(), 1);
|
||||
assert_eq!(media_tags[0].name, "Music");
|
||||
|
||||
// Untag
|
||||
storage.untag_media(id, parent.id).await.unwrap();
|
||||
let media_tags = storage.get_media_tags(id).await.unwrap();
|
||||
assert_eq!(media_tags.len(), 0);
|
||||
|
||||
// Delete tag
|
||||
storage.delete_tag(child.id).await.unwrap();
|
||||
let tags = storage.list_tags().await.unwrap();
|
||||
assert_eq!(tags.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_collections() {
|
||||
let storage = setup().await;
|
||||
|
||||
let col = storage
|
||||
.create_collection("Favorites", CollectionKind::Manual, Some("My faves"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(col.name, "Favorites");
|
||||
assert_eq!(col.kind, CollectionKind::Manual);
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let id = MediaId::new();
|
||||
let item = MediaItem {
|
||||
id,
|
||||
path: "/tmp/doc.pdf".into(),
|
||||
file_name: "doc.pdf".to_string(),
|
||||
media_type: pinakes_core::media_type::MediaType::Pdf,
|
||||
content_hash: ContentHash::new("pdfhash".to_string()),
|
||||
file_size: 10000,
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
genre: None,
|
||||
year: None,
|
||||
duration_secs: None,
|
||||
description: None,
|
||||
thumbnail_path: None,
|
||||
custom_fields: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
storage.insert_media(&item).await.unwrap();
|
||||
|
||||
storage.add_to_collection(col.id, id, 0).await.unwrap();
|
||||
let members = storage.get_collection_members(col.id).await.unwrap();
|
||||
assert_eq!(members.len(), 1);
|
||||
assert_eq!(members[0].id, id);
|
||||
|
||||
storage.remove_from_collection(col.id, id).await.unwrap();
|
||||
let members = storage.get_collection_members(col.id).await.unwrap();
|
||||
assert_eq!(members.len(), 0);
|
||||
|
||||
// List collections
|
||||
let cols = storage.list_collections().await.unwrap();
|
||||
assert_eq!(cols.len(), 1);
|
||||
|
||||
storage.delete_collection(col.id).await.unwrap();
|
||||
let cols = storage.list_collections().await.unwrap();
|
||||
assert_eq!(cols.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_fields() {
|
||||
let storage = setup().await;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let id = MediaId::new();
|
||||
let item = MediaItem {
|
||||
id,
|
||||
path: "/tmp/test.md".into(),
|
||||
file_name: "test.md".to_string(),
|
||||
media_type: pinakes_core::media_type::MediaType::Markdown,
|
||||
content_hash: ContentHash::new("mdhash".to_string()),
|
||||
file_size: 500,
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
genre: None,
|
||||
year: None,
|
||||
duration_secs: None,
|
||||
description: None,
|
||||
thumbnail_path: None,
|
||||
custom_fields: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
storage.insert_media(&item).await.unwrap();
|
||||
|
||||
// Set custom field
|
||||
let field = CustomField {
|
||||
field_type: CustomFieldType::Text,
|
||||
value: "important".to_string(),
|
||||
};
|
||||
storage
|
||||
.set_custom_field(id, "priority", &field)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get custom fields
|
||||
let fields = storage.get_custom_fields(id).await.unwrap();
|
||||
assert_eq!(fields.len(), 1);
|
||||
assert_eq!(fields["priority"].value, "important");
|
||||
|
||||
// Verify custom fields are loaded with get_media
|
||||
let media = storage.get_media(id).await.unwrap();
|
||||
assert_eq!(media.custom_fields.len(), 1);
|
||||
assert_eq!(media.custom_fields["priority"].value, "important");
|
||||
|
||||
// Delete custom field
|
||||
storage.delete_custom_field(id, "priority").await.unwrap();
|
||||
let fields = storage.get_custom_fields(id).await.unwrap();
|
||||
assert_eq!(fields.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search() {
|
||||
let storage = setup().await;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
// Insert a few items
|
||||
for (i, (name, title, artist)) in [
|
||||
("song1.mp3", "Bohemian Rhapsody", "Queen"),
|
||||
("song2.mp3", "Stairway to Heaven", "Led Zeppelin"),
|
||||
("doc.pdf", "Rust Programming", ""),
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let item = MediaItem {
|
||||
id: MediaId::new(),
|
||||
path: format!("/tmp/{name}").into(),
|
||||
file_name: name.to_string(),
|
||||
media_type: pinakes_core::media_type::MediaType::from_path(std::path::Path::new(name))
|
||||
.unwrap(),
|
||||
content_hash: ContentHash::new(format!("hash{i}")),
|
||||
file_size: 1000 * (i as u64 + 1),
|
||||
title: Some(title.to_string()),
|
||||
artist: if artist.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(artist.to_string())
|
||||
},
|
||||
album: None,
|
||||
genre: None,
|
||||
year: None,
|
||||
duration_secs: None,
|
||||
description: None,
|
||||
thumbnail_path: None,
|
||||
custom_fields: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
storage.insert_media(&item).await.unwrap();
|
||||
}
|
||||
|
||||
// Full-text search
|
||||
let request = pinakes_core::search::SearchRequest {
|
||||
query: pinakes_core::search::parse_search_query("Bohemian").unwrap(),
|
||||
sort: pinakes_core::search::SortOrder::Relevance,
|
||||
pagination: Pagination::new(0, 50, None),
|
||||
};
|
||||
let results = storage.search(&request).await.unwrap();
|
||||
assert_eq!(results.total_count, 1);
|
||||
assert_eq!(results.items[0].title.as_deref(), Some("Bohemian Rhapsody"));
|
||||
|
||||
// Type filter
|
||||
let request = pinakes_core::search::SearchRequest {
|
||||
query: pinakes_core::search::parse_search_query("type:pdf").unwrap(),
|
||||
sort: pinakes_core::search::SortOrder::Relevance,
|
||||
pagination: Pagination::new(0, 50, None),
|
||||
};
|
||||
let results = storage.search(&request).await.unwrap();
|
||||
assert_eq!(results.total_count, 1);
|
||||
assert_eq!(results.items[0].file_name, "doc.pdf");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_audit_log() {
|
||||
let storage = setup().await;
|
||||
|
||||
let entry = AuditEntry {
|
||||
id: uuid::Uuid::now_v7(),
|
||||
media_id: None,
|
||||
action: AuditAction::Scanned,
|
||||
details: Some("test scan".to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
};
|
||||
storage.record_audit(&entry).await.unwrap();
|
||||
|
||||
let entries = storage
|
||||
.list_audit_entries(None, &Pagination::new(0, 10, None))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].action, AuditAction::Scanned);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_with_dedup() {
|
||||
let storage = setup().await as pinakes_core::storage::DynStorageBackend;
|
||||
|
||||
// Create a temp file
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("test.txt");
|
||||
std::fs::write(&file_path, "hello world").unwrap();
|
||||
|
||||
// First import
|
||||
let result1 = pinakes_core::import::import_file(&storage, &file_path)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!result1.was_duplicate);
|
||||
|
||||
// Second import of same file
|
||||
let result2 = pinakes_core::import::import_file(&storage, &file_path)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(result2.was_duplicate);
|
||||
assert_eq!(result1.media_id, result2.media_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_root_dirs() {
|
||||
let storage = setup().await;
|
||||
|
||||
storage.add_root_dir("/tmp/music".into()).await.unwrap();
|
||||
storage.add_root_dir("/tmp/docs".into()).await.unwrap();
|
||||
|
||||
let dirs = storage.list_root_dirs().await.unwrap();
|
||||
assert_eq!(dirs.len(), 2);
|
||||
|
||||
storage
|
||||
.remove_root_dir(std::path::Path::new("/tmp/music"))
|
||||
.await
|
||||
.unwrap();
|
||||
let dirs = storage.list_root_dirs().await.unwrap();
|
||||
assert_eq!(dirs.len(), 1);
|
||||
assert_eq!(dirs[0], std::path::PathBuf::from("/tmp/docs"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_library_statistics_empty() {
|
||||
let storage = setup().await;
|
||||
let stats = storage.library_statistics().await.unwrap();
|
||||
assert_eq!(stats.total_media, 0);
|
||||
assert_eq!(stats.total_size_bytes, 0);
|
||||
assert_eq!(stats.avg_file_size_bytes, 0);
|
||||
assert!(stats.media_by_type.is_empty());
|
||||
assert!(stats.storage_by_type.is_empty());
|
||||
assert!(stats.top_tags.is_empty());
|
||||
assert!(stats.top_collections.is_empty());
|
||||
assert!(stats.newest_item.is_none());
|
||||
assert!(stats.oldest_item.is_none());
|
||||
assert_eq!(stats.total_tags, 0);
|
||||
assert_eq!(stats.total_collections, 0);
|
||||
assert_eq!(stats.total_duplicates, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_library_statistics_with_data() {
|
||||
let storage = setup().await;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let item = MediaItem {
|
||||
id: MediaId::new(),
|
||||
path: "/tmp/stats_test.mp3".into(),
|
||||
file_name: "stats_test.mp3".to_string(),
|
||||
media_type: pinakes_core::media_type::MediaType::Mp3,
|
||||
content_hash: ContentHash::new("stats_hash".to_string()),
|
||||
file_size: 5000,
|
||||
title: Some("Stats Song".to_string()),
|
||||
artist: None,
|
||||
album: None,
|
||||
genre: None,
|
||||
year: None,
|
||||
duration_secs: Some(120.0),
|
||||
description: None,
|
||||
thumbnail_path: None,
|
||||
custom_fields: HashMap::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
storage.insert_media(&item).await.unwrap();
|
||||
|
||||
let stats = storage.library_statistics().await.unwrap();
|
||||
assert_eq!(stats.total_media, 1);
|
||||
assert_eq!(stats.total_size_bytes, 5000);
|
||||
assert_eq!(stats.avg_file_size_bytes, 5000);
|
||||
assert!(!stats.media_by_type.is_empty());
|
||||
assert!(stats.newest_item.is_some());
|
||||
assert!(stats.oldest_item.is_some());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue