feat(db): enforce required-field completeness on publish (#16)
set_visibility now gates the transition to Public: every field definition with required=true must have a value on the object (typed inventory-minimum columns are already NOT NULL, so only flexible required fields are checked). Missing values yield VisibilityError::MissingRequiredFields(keys); the admin publish endpoint maps it to 422. The gate runs in db so every caller is protected and the check is atomic with the transition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -184,3 +184,75 @@ async fn public_reads_return_only_public_records(pool: PgPool) {
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn publishing_requires_all_required_fields_present(pool: PgPool) {
|
||||
use db::fields;
|
||||
use domain::{FieldType, LocalizedLabel, NewFieldDefinition};
|
||||
|
||||
let db = Db::from_pool(pool);
|
||||
|
||||
let mut tx = db.pool().begin().await.unwrap();
|
||||
|
||||
// a required flexible field
|
||||
fields::create_field_definition(
|
||||
&mut tx,
|
||||
&NewFieldDefinition {
|
||||
key: "inscription".into(),
|
||||
field_type: FieldType::Text,
|
||||
required: true,
|
||||
group_key: None,
|
||||
labels: vec![LocalizedLabel {
|
||||
lang: "en".into(),
|
||||
label: "Inscription".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = catalog::create_object(
|
||||
&mut tx,
|
||||
AuditActor::System,
|
||||
&object("LM-1", Visibility::Draft),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Internal)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// publishing without the required field present is rejected
|
||||
let err = catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Public)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, catalog::VisibilityError::MissingRequiredFields(ref keys) if keys == &["inscription"])
|
||||
);
|
||||
|
||||
// the object is still not public
|
||||
let still = catalog::object_by_id(&mut *tx, id).await.unwrap().unwrap();
|
||||
assert_eq!(still.visibility, Visibility::Internal);
|
||||
|
||||
// set the required field, then publishing succeeds
|
||||
catalog::set_object_fields(
|
||||
&mut tx,
|
||||
AuditActor::System,
|
||||
id,
|
||||
serde_json::json!({ "inscription": "To the gods" })
|
||||
.as_object()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Public)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
let published = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
|
||||
assert_eq!(published.visibility, Visibility::Public);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user