diff --git a/crates/db/tests/object_fields.rs b/crates/db/tests/object_fields.rs index 7c14c53..8d8a24f 100644 --- a/crates/db/tests/object_fields.rs +++ b/crates/db/tests/object_fields.rs @@ -163,3 +163,212 @@ async fn unknown_field_and_type_mismatch_are_rejected(pool: PgPool) { )); drop(tx); } + +#[sqlx::test] +async fn authority_field_enforces_kind(pool: PgPool) { + let db = Db::from_pool(pool); + let id = setup_object(&db).await; + define( + &db, + "maker", + FieldType::Authority { + kind: Some(domain::AuthorityKind::Person), + }, + ) + .await; + + let mut tx = db.pool().begin().await.unwrap(); + let person = db::authority::create_authority( + &mut tx, + &domain::NewAuthority { + kind: domain::AuthorityKind::Person, + external_uri: None, + labels: label("Carl"), + }, + ) + .await + .unwrap(); + let place = db::authority::create_authority( + &mut tx, + &domain::NewAuthority { + kind: domain::AuthorityKind::Place, + external_uri: None, + labels: label("Stockholm"), + }, + ) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let ok = serde_json::json!({ "maker": person.to_string() }); + let mut tx = db.pool().begin().await.unwrap(); + catalog::set_object_fields(&mut tx, AuditActor::System, id, ok.as_object().unwrap()) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let bad = serde_json::json!({ "maker": place.to_string() }); + let mut tx = db.pool().begin().await.unwrap(); + assert!(matches!( + catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await, + Err(FieldError::Unresolved { .. }) + )); +} + +#[sqlx::test] +async fn term_from_wrong_vocabulary_is_rejected(pool: PgPool) { + let db = Db::from_pool(pool); + let id = setup_object(&db).await; + let material = vocab::create_vocabulary(db.pool(), "material") + .await + .unwrap(); + let technique = vocab::create_vocabulary(db.pool(), "technique") + .await + .unwrap(); + define( + &db, + "material", + FieldType::Term { + vocabulary_id: material.id, + }, + ) + .await; + + // a real term, but in the WRONG vocabulary + let mut tx = db.pool().begin().await.unwrap(); + let other = vocab::add_term( + &mut tx, + &domain::NewTerm { + vocabulary_id: technique.id, + external_uri: None, + labels: label("forged"), + }, + ) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let bad = serde_json::json!({ "material": other.to_string() }); + let mut tx = db.pool().begin().await.unwrap(); + assert!(matches!( + catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await, + Err(FieldError::Unresolved { .. }) + )); +} + +#[sqlx::test] +async fn localized_text_round_trips(pool: PgPool) { + let db = Db::from_pool(pool); + let id = setup_object(&db).await; + define(&db, "title", FieldType::LocalizedText).await; + + let values = serde_json::json!({ "title": { "sv": "Vas", "en": "Vase" } }); + let mut tx = db.pool().begin().await.unwrap(); + catalog::set_object_fields(&mut tx, AuditActor::System, id, values.as_object().unwrap()) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap(); + assert_eq!(obj.fields["title"]["sv"], "Vas"); + assert_eq!(obj.fields["title"]["en"], "Vase"); + + let bad = serde_json::json!({ "title": { "sv": 5 } }); + let mut tx = db.pool().begin().await.unwrap(); + assert!(matches!( + catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await, + Err(FieldError::TypeMismatch { .. }) + )); +} + +#[sqlx::test] +async fn replace_semantics_remove_a_field_and_audit_it(pool: PgPool) { + let db = Db::from_pool(pool); + let id = setup_object(&db).await; + define(&db, "comments", FieldType::Text).await; + define(&db, "year", FieldType::Integer).await; + + let mut tx = db.pool().begin().await.unwrap(); + catalog::set_object_fields( + &mut tx, + AuditActor::System, + id, + serde_json::json!({ "comments": "x", "year": 1850 }) + .as_object() + .unwrap(), + ) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let mut tx = db.pool().begin().await.unwrap(); + catalog::set_object_fields( + &mut tx, + AuditActor::System, + id, + serde_json::json!({ "comments": "x" }).as_object().unwrap(), + ) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap(); + assert!(obj.fields.get("year").is_none()); + + let history = audit::history_for(db.pool(), "object", id.to_uuid()) + .await + .unwrap(); + let last = history.last().unwrap(); + let year = last + .changes + .iter() + .find(|c| c.field == "year") + .expect("year removal recorded"); + assert!(year.before.is_some()); + assert!(year.after.is_none()); +} + +#[sqlx::test] +async fn no_op_set_records_no_audit(pool: PgPool) { + let db = Db::from_pool(pool); + let id = setup_object(&db).await; + define(&db, "comments", FieldType::Text).await; + + let values = serde_json::json!({ "comments": "x" }); + let mut tx = db.pool().begin().await.unwrap(); + catalog::set_object_fields(&mut tx, AuditActor::System, id, values.as_object().unwrap()) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let before = audit::history_for(db.pool(), "object", id.to_uuid()) + .await + .unwrap() + .len(); + + let mut tx = db.pool().begin().await.unwrap(); + catalog::set_object_fields(&mut tx, AuditActor::System, id, values.as_object().unwrap()) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let after = audit::history_for(db.pool(), "object", id.to_uuid()) + .await + .unwrap() + .len(); + assert_eq!(before, after, "a no-op set must not add an audit entry"); +} + +#[sqlx::test] +async fn set_on_missing_object_errors(pool: PgPool) { + let db = Db::from_pool(pool); + let mut tx = db.pool().begin().await.unwrap(); + let err = catalog::set_object_fields( + &mut tx, + AuditActor::System, + domain::ObjectId::new(), + serde_json::json!({}).as_object().unwrap(), + ) + .await; + assert!(matches!(err, Err(FieldError::ObjectNotFound))); +}