From 4a775b5e9ba63a1accd5ffddb97b2f919c695a0c Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 18 Apr 2026 18:25:47 -0400 Subject: [PATCH] test: cover resolve_library_param and per-library ExifDao filter Adds 9 unit tests around the library plumbing: - resolve_library_param branches (absent, empty/whitespace, numeric id, name, unknown id, unknown name) - Library::resolve symmetry with strip_root - ExifDao::get_all_with_date_taken in union and scoped modes Introduces SqliteExifDao::from_connection test constructor mirroring the existing preview_dao pattern so DAO tests can drive an in-memory SQLite. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/database/mod.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++ src/libraries.rs | 81 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/src/database/mod.rs b/src/database/mod.rs index fe0957c..e42db69 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -403,6 +403,13 @@ impl SqliteExifDao { connection: Arc::new(Mutex::new(connect())), } } + + #[cfg(test)] + pub fn from_connection(conn: SqliteConnection) -> Self { + SqliteExifDao { + connection: Arc::new(Mutex::new(conn)), + } + } } impl ExifDao for SqliteExifDao { @@ -927,3 +934,92 @@ impl ExifDao for SqliteExifDao { .map_err(|_| DbError::new(DbErrorKind::QueryError)) } } + +#[cfg(test)] +mod exif_dao_tests { + use super::*; + use crate::database::models::InsertLibrary; + use crate::database::test::in_memory_db_connection; + + fn ctx() -> opentelemetry::Context { + opentelemetry::Context::new() + } + + fn insert_row(dao: &mut SqliteExifDao, lib_id: i32, rel: &str, date: Option) { + dao.store_exif( + &ctx(), + InsertImageExif { + library_id: lib_id, + file_path: rel.to_string(), + camera_make: None, + camera_model: None, + lens_model: None, + width: None, + height: None, + orientation: None, + gps_latitude: None, + gps_longitude: None, + gps_altitude: None, + focal_length: None, + aperture: None, + shutter_speed: None, + iso: None, + date_taken: date, + created_time: 0, + last_modified: 0, + content_hash: None, + size_bytes: None, + }, + ) + .expect("insert exif row"); + } + + fn setup_two_libraries() -> SqliteExifDao { + let mut conn = in_memory_db_connection(); + // Migration seeds library id=1 with a placeholder root; add id=2. + diesel::insert_into(schema::libraries::table) + .values(InsertLibrary { + name: "archive", + root_path: "/tmp/archive", + created_at: 0, + }) + .execute(&mut conn) + .expect("seed second library"); + SqliteExifDao::from_connection(conn) + } + + #[test] + fn get_all_with_date_taken_union_returns_all_libraries() { + let mut dao = setup_two_libraries(); + insert_row(&mut dao, 1, "main/a.jpg", Some(100)); + insert_row(&mut dao, 2, "archive/b.jpg", Some(200)); + // Row without a date must be excluded even in union mode. + insert_row(&mut dao, 2, "archive/c.jpg", None); + + let mut rows = dao.get_all_with_date_taken(&ctx(), None).unwrap(); + rows.sort_by_key(|(_, ts)| *ts); + assert_eq!( + rows, + vec![ + ("main/a.jpg".to_string(), 100), + ("archive/b.jpg".to_string(), 200), + ] + ); + } + + #[test] + fn get_all_with_date_taken_scopes_by_library_id() { + let mut dao = setup_two_libraries(); + insert_row(&mut dao, 1, "main/a.jpg", Some(100)); + insert_row(&mut dao, 2, "archive/b.jpg", Some(200)); + insert_row(&mut dao, 2, "archive/c.jpg", Some(300)); + + let lib2 = dao.get_all_with_date_taken(&ctx(), Some(2)).unwrap(); + let mut paths: Vec = lib2.into_iter().map(|(p, _)| p).collect(); + paths.sort(); + assert_eq!(paths, vec!["archive/b.jpg", "archive/c.jpg"]); + + let lib1 = dao.get_all_with_date_taken(&ctx(), Some(1)).unwrap(); + assert_eq!(lib1, vec![("main/a.jpg".to_string(), 100)]); + } +} diff --git a/src/libraries.rs b/src/libraries.rs index 3cfc0be..cc3f2f4 100644 --- a/src/libraries.rs +++ b/src/libraries.rs @@ -198,4 +198,85 @@ mod tests { let outside = lib.strip_root(Path::new("/etc/passwd")); assert!(outside.is_none()); } + + #[test] + fn library_resolve_joins_under_root() { + let lib = Library { + id: 1, + name: "main".into(), + root_path: "/tmp/media".into(), + }; + let abs = lib.resolve("2024/photo.jpg"); + assert_eq!(abs, PathBuf::from("/tmp/media/2024/photo.jpg")); + } + + fn state_with_libraries(libs: Vec) -> AppState { + let mut state = AppState::test_state(); + state.libraries = libs; + state + } + + fn sample_libraries() -> Vec { + vec![ + Library { + id: 1, + name: "main".into(), + root_path: "/tmp/main".into(), + }, + Library { + id: 7, + name: "archive".into(), + root_path: "/tmp/archive".into(), + }, + ] + } + + #[actix_rt::test] + async fn resolve_library_param_absent_is_union() { + let state = state_with_libraries(sample_libraries()); + assert!(matches!(resolve_library_param(&state, None), Ok(None))); + } + + #[actix_rt::test] + async fn resolve_library_param_empty_or_whitespace_is_union() { + let state = state_with_libraries(sample_libraries()); + assert!(matches!(resolve_library_param(&state, Some("")), Ok(None))); + assert!(matches!( + resolve_library_param(&state, Some(" ")), + Ok(None) + )); + } + + #[actix_rt::test] + async fn resolve_library_param_numeric_id_matches() { + let state = state_with_libraries(sample_libraries()); + let lib = resolve_library_param(&state, Some("7")) + .expect("valid id") + .expect("some library"); + assert_eq!(lib.id, 7); + assert_eq!(lib.name, "archive"); + } + + #[actix_rt::test] + async fn resolve_library_param_name_matches() { + let state = state_with_libraries(sample_libraries()); + let lib = resolve_library_param(&state, Some("main")) + .expect("valid name") + .expect("some library"); + assert_eq!(lib.id, 1); + } + + #[actix_rt::test] + async fn resolve_library_param_unknown_id_errs() { + let state = state_with_libraries(sample_libraries()); + let err = resolve_library_param(&state, Some("999")).unwrap_err(); + assert!(err.contains("unknown library id")); + } + + #[actix_rt::test] + async fn resolve_library_param_unknown_name_errs() { + let state = state_with_libraries(sample_libraries()); + let err = resolve_library_param(&state, Some("missing")).unwrap_err(); + assert!(err.contains("unknown library name")); + } }