004 Multi-library Support #54
@@ -403,6 +403,13 @@ impl SqliteExifDao {
|
|||||||
connection: Arc::new(Mutex::new(connect())),
|
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 {
|
impl ExifDao for SqliteExifDao {
|
||||||
@@ -927,3 +934,92 @@ impl ExifDao for SqliteExifDao {
|
|||||||
.map_err(|_| DbError::new(DbErrorKind::QueryError))
|
.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<i64>) {
|
||||||
|
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<String> = 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)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -198,4 +198,85 @@ mod tests {
|
|||||||
let outside = lib.strip_root(Path::new("/etc/passwd"));
|
let outside = lib.strip_root(Path::new("/etc/passwd"));
|
||||||
assert!(outside.is_none());
|
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<Library>) -> AppState {
|
||||||
|
let mut state = AppState::test_state();
|
||||||
|
state.libraries = libs;
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_libraries() -> Vec<Library> {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user