Files
MusicFS/crates/musicfs-client/tests/control_test.rs
T
2026-07-01 20:23:10 +02:00

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());
}