Define endpoints to update metadata
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user