288 lines
9.4 KiB
Rust
288 lines
9.4 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
sync::{Arc, Mutex},
|
|
time::SystemTime,
|
|
};
|
|
|
|
use fuser::INodeNo;
|
|
use musicfs::control::ClientControlServiceImpl;
|
|
use musicfs::item::{FileType, Item};
|
|
use musicfs::music::db::entities::{artists, music_metadata as mm_entity};
|
|
use musicfs::music::metadata::MusicMetadata;
|
|
use musicfs::origins::attrs::FileAttrs;
|
|
use musicfs_proto::{ClientControl, GetMusicMetadataRequest, UpdateMusicMetadataRequest};
|
|
use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult};
|
|
use tonic::{Code, Request};
|
|
|
|
// ── Test helpers ───────────────────────────────────────────────────────
|
|
|
|
fn make_attrs() -> FileAttrs {
|
|
FileAttrs {
|
|
size: 4096,
|
|
blocks: 8,
|
|
atime: SystemTime::UNIX_EPOCH,
|
|
mtime: SystemTime::UNIX_EPOCH,
|
|
ctime: SystemTime::UNIX_EPOCH,
|
|
crtime: SystemTime::UNIX_EPOCH,
|
|
perm: 0o644,
|
|
nlink: 1,
|
|
uid: 0,
|
|
gid: 0,
|
|
rdev: 0,
|
|
blksize: 4096,
|
|
}
|
|
}
|
|
|
|
fn make_flac_item(inode: u64, name: &str, metadata: Option<MusicMetadata>) -> Item {
|
|
Item {
|
|
inode: INodeNo(inode),
|
|
parent_inode: INodeNo(1),
|
|
name: name.to_string(),
|
|
original_path: format!("/{name}").into(),
|
|
local_path: format!("Artist/Album/{name}").into(),
|
|
file_type: FileType::File,
|
|
attrs: make_attrs(),
|
|
music_metadata: metadata,
|
|
hash: inode,
|
|
}
|
|
}
|
|
|
|
fn make_flac_metadata() -> MusicMetadata {
|
|
MusicMetadata {
|
|
artist: vec!["Test Artist".to_string()],
|
|
album_artist: Some("Test Artist".to_string()),
|
|
album: "Test Album".to_string(),
|
|
track_number: 1,
|
|
track_title: "Test Track".to_string(),
|
|
other_tags: vec![],
|
|
header: vec![],
|
|
picture_block_headers: vec![],
|
|
picture_data_ranges: vec![],
|
|
real_audio_start: 0,
|
|
vorbis_comment_offset: 0,
|
|
vorbis_comment_length: 0,
|
|
}
|
|
}
|
|
|
|
/// Mock DB for get tests (no INSERT operations expected).
|
|
fn mock_db_readonly() -> sea_orm::DatabaseConnection {
|
|
MockDatabase::new(DatabaseBackend::Postgres).into_connection()
|
|
}
|
|
|
|
/// Mock DB for update with one artist.
|
|
/// save_music_metadata does: DELETE, INSERT music_metadata, INSERT artists.
|
|
fn mock_db_update_with_artist(inode: i64) -> sea_orm::DatabaseConnection {
|
|
MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_exec_results([MockExecResult {
|
|
rows_affected: 1,
|
|
..Default::default()
|
|
}])
|
|
.append_query_results([vec![mm_entity::Model {
|
|
inode,
|
|
track_title: String::new(),
|
|
album: String::new(),
|
|
track_number: 0,
|
|
header: vec![],
|
|
real_audio_start: 0,
|
|
}]])
|
|
.append_query_results([vec![artists::Model {
|
|
inode,
|
|
artist: String::new(),
|
|
}]])
|
|
.into_connection()
|
|
}
|
|
|
|
/// Mock DB for update with no artists.
|
|
/// save_music_metadata does: DELETE, INSERT music_metadata only.
|
|
fn mock_db_update_no_artist(inode: i64) -> sea_orm::DatabaseConnection {
|
|
MockDatabase::new(DatabaseBackend::Postgres)
|
|
.append_exec_results([MockExecResult {
|
|
rows_affected: 1,
|
|
..Default::default()
|
|
}])
|
|
.append_query_results([vec![mm_entity::Model {
|
|
inode,
|
|
track_title: String::new(),
|
|
album: String::new(),
|
|
track_number: 0,
|
|
header: vec![],
|
|
real_audio_start: 0,
|
|
}]])
|
|
.into_connection()
|
|
}
|
|
|
|
// ── GetMusicMetadata ──────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn get_returns_metadata_for_valid_inode() {
|
|
let mut files = BTreeMap::new();
|
|
files.insert(
|
|
INodeNo(100),
|
|
make_flac_item(100, "song.flac", Some(make_flac_metadata())),
|
|
);
|
|
let files = Arc::new(Mutex::new(files));
|
|
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
|
|
|
|
let resp = svc
|
|
.get_music_metadata(Request::new(GetMusicMetadataRequest { inode: 100 }))
|
|
.await
|
|
.expect("get should succeed");
|
|
|
|
let md = resp.into_inner().metadata.expect("metadata present");
|
|
assert_eq!(md.track_title, "Test Track");
|
|
assert_eq!(md.album, "Test Album");
|
|
assert_eq!(md.artist, vec!["Test Artist".to_string()]);
|
|
assert_eq!(md.track_number, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn get_returns_not_found_for_missing_inode() {
|
|
let files = Arc::new(Mutex::new(BTreeMap::new()));
|
|
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
|
|
|
|
let err = svc
|
|
.get_music_metadata(Request::new(GetMusicMetadataRequest { inode: 999 }))
|
|
.await
|
|
.expect_err("should be NOT_FOUND");
|
|
|
|
assert_eq!(err.code(), Code::NotFound);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn get_returns_not_found_when_item_has_no_metadata() {
|
|
let mut files = BTreeMap::new();
|
|
files.insert(INodeNo(100), make_flac_item(100, "song.flac", None));
|
|
let files = Arc::new(Mutex::new(files));
|
|
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
|
|
|
|
let err = svc
|
|
.get_music_metadata(Request::new(GetMusicMetadataRequest { inode: 100 }))
|
|
.await
|
|
.expect_err("should be NOT_FOUND");
|
|
|
|
assert_eq!(err.code(), Code::NotFound);
|
|
}
|
|
|
|
// ── UpdateMusicMetadata ───────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn update_changes_tags_and_response_reflects_new_values() {
|
|
let mut files = BTreeMap::new();
|
|
files.insert(
|
|
INodeNo(100),
|
|
make_flac_item(100, "song.flac", Some(make_flac_metadata())),
|
|
);
|
|
let files = Arc::new(Mutex::new(files));
|
|
let svc = ClientControlServiceImpl::new(files.clone(), mock_db_update_with_artist(100));
|
|
|
|
let resp = svc
|
|
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
|
|
inode: 100,
|
|
artist: vec!["Updated Artist".to_string()],
|
|
album_artist: Some("Updated Artist".to_string()),
|
|
album: "Updated Album".to_string(),
|
|
track_number: 7,
|
|
track_title: "Updated Title".to_string(),
|
|
other_tags: vec![],
|
|
}))
|
|
.await
|
|
.expect("update should succeed");
|
|
|
|
let md = resp.into_inner().metadata.expect("metadata present");
|
|
assert_eq!(md.track_title, "Updated Title");
|
|
assert_eq!(md.album, "Updated Album");
|
|
assert_eq!(md.artist, vec!["Updated Artist".to_string()]);
|
|
assert_eq!(md.track_number, 7);
|
|
|
|
let guard = files.lock().unwrap();
|
|
let item = guard.get(&INodeNo(100)).expect("item still in map");
|
|
let mm = item.music_metadata.as_ref().expect("metadata present");
|
|
assert_eq!(mm.track_title, "Updated Title");
|
|
assert_eq!(mm.album, "Updated Album");
|
|
assert_eq!(mm.artist, vec!["Updated Artist".to_string()]);
|
|
assert_eq!(mm.track_number, 7);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn update_returns_not_found_for_missing_inode() {
|
|
let files = Arc::new(Mutex::new(BTreeMap::new()));
|
|
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
|
|
|
|
let err = svc
|
|
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
|
|
inode: 999,
|
|
artist: vec![],
|
|
album_artist: None,
|
|
album: String::new(),
|
|
track_number: 0,
|
|
track_title: String::new(),
|
|
other_tags: vec![],
|
|
}))
|
|
.await
|
|
.expect_err("should be NOT_FOUND");
|
|
|
|
assert_eq!(err.code(), Code::NotFound);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn update_returns_not_found_when_item_has_no_metadata() {
|
|
let mut files = BTreeMap::new();
|
|
files.insert(INodeNo(100), make_flac_item(100, "song.flac", None));
|
|
let files = Arc::new(Mutex::new(files));
|
|
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
|
|
|
|
let err = svc
|
|
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
|
|
inode: 100,
|
|
artist: vec!["X".to_string()],
|
|
album_artist: None,
|
|
album: "X".to_string(),
|
|
track_number: 1,
|
|
track_title: "X".to_string(),
|
|
other_tags: vec![],
|
|
}))
|
|
.await
|
|
.expect_err("should be NOT_FOUND");
|
|
|
|
assert_eq!(err.code(), Code::NotFound);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn update_clears_tags_when_empty_values_sent() {
|
|
let mut files = BTreeMap::new();
|
|
files.insert(
|
|
INodeNo(100),
|
|
make_flac_item(100, "song.flac", Some(make_flac_metadata())),
|
|
);
|
|
let files = Arc::new(Mutex::new(files));
|
|
let svc = ClientControlServiceImpl::new(files.clone(), mock_db_update_no_artist(100));
|
|
|
|
let resp = svc
|
|
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
|
|
inode: 100,
|
|
artist: vec![],
|
|
album_artist: None,
|
|
album: String::new(),
|
|
track_number: 0,
|
|
track_title: String::new(),
|
|
other_tags: vec![],
|
|
}))
|
|
.await
|
|
.expect("update should succeed");
|
|
|
|
let md = resp.into_inner().metadata.expect("metadata present");
|
|
assert!(md.artist.is_empty());
|
|
assert!(md.album.is_empty());
|
|
assert!(md.track_title.is_empty());
|
|
|
|
let guard = files.lock().unwrap();
|
|
let mm = guard
|
|
.get(&INodeNo(100))
|
|
.unwrap()
|
|
.music_metadata
|
|
.as_ref()
|
|
.unwrap();
|
|
assert!(mm.artist.is_empty());
|
|
assert!(mm.album.is_empty());
|
|
}
|