use db::{Db, audit, catalog, fields, vocab}; use domain::{ AuditAction, AuditActor, FieldType, LocalizedLabel, NewFieldDefinition, NewTerm, ObjectInput, Visibility, }; use sqlx::PgPool; #[sqlx::test] async fn vocabulary_create_and_lookup(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let v = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); tx.commit().await.unwrap(); let found = vocab::vocabulary_by_key(db.pool(), "material") .await .unwrap() .unwrap(); assert_eq!(found.id, v.id); assert_eq!(found.key, "material"); assert!( vocab::vocabulary_by_key(db.pool(), "nope") .await .unwrap() .is_none() ); } #[sqlx::test] async fn term_with_multilingual_labels_round_trips(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let v = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); tx.commit().await.unwrap(); let mut tx = db.pool().begin().await.unwrap(); let term_id = vocab::add_term( &mut tx, AuditActor::System, &NewTerm { vocabulary_id: v.id, external_uri: Some("http://vocab.getty.edu/aat/300011914".into()), labels: vec![ LocalizedLabel { lang: "sv".into(), label: "trä".into(), }, LocalizedLabel { lang: "en".into(), label: "wood".into(), }, ], }, ) .await .unwrap(); tx.commit().await.unwrap(); let term = vocab::term_by_id(db.pool(), term_id) .await .unwrap() .unwrap(); assert_eq!(term.vocabulary_id, v.id); assert_eq!( term.external_uri.as_deref(), Some("http://vocab.getty.edu/aat/300011914") ); assert_eq!(term.labels.len(), 2); assert_eq!(domain::pick_label(&term.labels, "sv", "en"), Some("trä")); assert_eq!(domain::pick_label(&term.labels, "de", "en"), Some("wood")); let listed = vocab::list_terms(db.pool(), v.id).await.unwrap(); assert_eq!(listed.len(), 1); assert_eq!(listed[0].id, term_id); } #[sqlx::test] async fn term_with_no_labels_round_trips_empty(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let v = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); tx.commit().await.unwrap(); let mut tx = db.pool().begin().await.unwrap(); let term_id = vocab::add_term( &mut tx, AuditActor::System, &NewTerm { vocabulary_id: v.id, external_uri: None, labels: vec![], }, ) .await .unwrap(); tx.commit().await.unwrap(); let term = vocab::term_by_id(db.pool(), term_id) .await .unwrap() .unwrap(); assert!(term.labels.is_empty()); } #[sqlx::test] async fn duplicate_vocabulary_key_is_rejected(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); tx.commit().await.unwrap(); let mut tx = db.pool().begin().await.unwrap(); let err = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap_err(); assert!( matches!(err, sqlx::Error::Database(_)), "expected a unique-violation DB error, got: {err:?}" ); } #[sqlx::test] async fn resolve_term_checks_vocabulary_membership(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let material = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); let technique = vocab::create_vocabulary(&mut tx, AuditActor::System, "technique") .await .unwrap(); tx.commit().await.unwrap(); let mut tx = db.pool().begin().await.unwrap(); let term_id = vocab::add_term( &mut tx, AuditActor::System, &NewTerm { vocabulary_id: material.id, external_uri: None, labels: vec![LocalizedLabel { lang: "en".into(), label: "wood".into(), }], }, ) .await .unwrap(); tx.commit().await.unwrap(); assert!( vocab::resolve_term(db.pool(), material.id, term_id) .await .unwrap() .is_some() ); assert!( vocab::resolve_term(db.pool(), technique.id, term_id) .await .unwrap() .is_none() ); } fn sample_object_input() -> ObjectInput { ObjectInput { object_number: "X.1".into(), object_name: "Test".into(), number_of_objects: 1, brief_description: None, current_location: None, current_owner: None, recorder: None, recording_date: None, visibility: Visibility::Draft, } } #[sqlx::test(migrations = "../db/migrations")] async fn update_term_changes_labels_and_uri(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let vocab = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); let term_id = vocab::add_term( &mut tx, AuditActor::System, &NewTerm { vocabulary_id: vocab.id, external_uri: None, labels: vec![LocalizedLabel { lang: "sv".into(), label: "Trä".into(), }], }, ) .await .unwrap(); let existed = vocab::update_term( &mut tx, AuditActor::System, vocab.id, term_id, Some("https://example.org/wood"), &[LocalizedLabel { lang: "sv".into(), label: "Träslag".into(), }], ) .await .unwrap(); assert!(existed); let history = audit::history_for(&mut *tx, "term", term_id.to_uuid()) .await .unwrap(); assert!( history.iter().any(|e| e.action == AuditAction::Updated), "expected an Updated audit entry for the term" ); tx.commit().await.unwrap(); let term = vocab::term_by_id(db.pool(), term_id) .await .unwrap() .unwrap(); assert_eq!( term.external_uri.as_deref(), Some("https://example.org/wood") ); assert_eq!(term.labels.len(), 1); assert_eq!(term.labels[0].label, "Träslag"); } #[sqlx::test(migrations = "../db/migrations")] async fn delete_term_blocks_when_referenced_then_succeeds(pool: PgPool) { use db::DeleteOutcome; let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let vocab = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); let term_id = vocab::add_term( &mut tx, AuditActor::System, &NewTerm { vocabulary_id: vocab.id, external_uri: None, labels: vec![LocalizedLabel { lang: "sv".into(), label: "Trä".into(), }], }, ) .await .unwrap(); fields::create_field_definition( &mut tx, &NewFieldDefinition { key: "material".into(), field_type: FieldType::Term { vocabulary_id: vocab.id, }, required: false, group_key: None, labels: vec![LocalizedLabel { lang: "sv".into(), label: "Material".into(), }], }, ) .await .unwrap(); let obj = catalog::create_object(&mut tx, AuditActor::System, &sample_object_input()) .await .unwrap(); let mut map = serde_json::Map::new(); map.insert( "material".into(), serde_json::Value::String(term_id.to_string()), ); catalog::set_object_fields(&mut tx, AuditActor::System, obj, &map) .await .unwrap(); let blocked = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id) .await .unwrap(); assert_eq!(blocked, DeleteOutcome::InUse { count: 1 }); catalog::set_object_fields(&mut tx, AuditActor::System, obj, &serde_json::Map::new()) .await .unwrap(); let ok = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id) .await .unwrap(); assert_eq!(ok, DeleteOutcome::Deleted); assert!( vocab::term_by_id(&mut *tx, term_id) .await .unwrap() .is_none() ); let history = audit::history_for(&mut *tx, "term", term_id.to_uuid()) .await .unwrap(); assert!( history.iter().any(|e| e.action == AuditAction::Deleted), "expected a Deleted audit entry for the term" ); let gone = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id) .await .unwrap(); assert_eq!(gone, DeleteOutcome::NotFound); } #[sqlx::test(migrations = "../db/migrations")] async fn rename_vocabulary_changes_key(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let v = vocab::create_vocabulary(&mut tx, AuditActor::System, "old") .await .unwrap(); let existed = vocab::rename_vocabulary(&mut tx, AuditActor::System, v.id, "new") .await .unwrap(); assert!(existed); tx.commit().await.unwrap(); assert!( vocab::vocabulary_by_key(db.pool(), "new") .await .unwrap() .is_some() ); assert!( vocab::vocabulary_by_key(db.pool(), "old") .await .unwrap() .is_none() ); } #[sqlx::test(migrations = "../db/migrations")] async fn delete_vocabulary_blocks_when_it_has_terms(pool: PgPool) { use db::DeleteOutcome; let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let v = vocab::create_vocabulary(&mut tx, AuditActor::System, "material") .await .unwrap(); vocab::add_term( &mut tx, AuditActor::System, &NewTerm { vocabulary_id: v.id, external_uri: None, labels: vec![LocalizedLabel { lang: "sv".into(), label: "Trä".into(), }], }, ) .await .unwrap(); let blocked = vocab::delete_vocabulary(&mut tx, AuditActor::System, v.id) .await .unwrap(); assert_eq!(blocked, DeleteOutcome::InUse { count: 1 }); let empty = vocab::create_vocabulary(&mut tx, AuditActor::System, "empty") .await .unwrap(); assert_eq!( vocab::delete_vocabulary(&mut tx, AuditActor::System, empty.id) .await .unwrap(), DeleteOutcome::Deleted ); let gone = vocab::delete_vocabulary(&mut tx, AuditActor::System, empty.id) .await .unwrap(); assert_eq!(gone, DeleteOutcome::NotFound); }