test(db): cover authority-kind, cross-vocabulary, localized text, replace/remove, no-op, missing object

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 11:07:43 +02:00
parent c94fd1638c
commit 45c1d1b123
+209
View File
@@ -163,3 +163,212 @@ async fn unknown_field_and_type_mismatch_are_rejected(pool: PgPool) {
)); ));
drop(tx); 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)));
}