use db::catalog::FieldError; use db::{Db, audit, catalog, fields, vocab}; use domain::{ AuditAction, AuditActor, FieldType, LocalizedLabel, NewFieldDefinition, ObjectInput, Visibility, }; use sqlx::PgPool; fn obj_input() -> ObjectInput { ObjectInput { object_number: "LM-1".into(), object_name: "vase".into(), number_of_objects: 1, brief_description: None, current_location: None, current_owner: None, recorder: None, recording_date: None, visibility: Visibility::Draft, } } fn label(text: &str) -> Vec { vec![LocalizedLabel { lang: "en".into(), label: text.into(), }] } async fn setup_object(db: &Db) -> domain::ObjectId { let mut tx = db.pool().begin().await.unwrap(); let id = catalog::create_object(&mut tx, AuditActor::System, &obj_input()) .await .unwrap(); tx.commit().await.unwrap(); id } async fn define(db: &Db, key: &str, field_type: FieldType) { let mut tx = db.pool().begin().await.unwrap(); fields::create_field_definition( &mut tx, &NewFieldDefinition { key: key.into(), field_type, required: false, group_key: None, labels: label(key), }, ) .await .unwrap(); tx.commit().await.unwrap(); } #[sqlx::test] async fn sets_scalar_fields_and_audits(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; define(&db, "on_display", FieldType::Boolean).await; let values = serde_json::json!({ "comments": "nice", "year": 1850, "on_display": true }); 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["comments"], "nice"); assert_eq!(obj.fields["year"], 1850); assert_eq!(obj.fields["on_display"], true); let history = audit::history_for(db.pool(), "object", id.to_uuid()) .await .unwrap(); assert_eq!(history.last().unwrap().action, AuditAction::Updated); let changed: Vec<&str> = history .last() .unwrap() .changes .iter() .map(|c| c.field.as_str()) .collect(); assert!( changed.contains(&"comments") && changed.contains(&"year") && changed.contains(&"on_display") ); } #[sqlx::test] async fn term_field_must_resolve_in_its_vocabulary(pool: PgPool) { let db = Db::from_pool(pool); let id = setup_object(&db).await; let material = vocab::create_vocabulary(db.pool(), "material") .await .unwrap(); define( &db, "material", FieldType::Term { vocabulary_id: material.id, }, ) .await; let mut tx = db.pool().begin().await.unwrap(); let wood = vocab::add_term( &mut tx, &domain::NewTerm { vocabulary_id: material.id, external_uri: None, labels: label("wood"), }, ) .await .unwrap(); tx.commit().await.unwrap(); let ok = serde_json::json!({ "material": wood.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!({ "material": domain::TermId::new().to_string() }); let mut tx = db.pool().begin().await.unwrap(); let err = catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await; assert!(matches!(err, Err(FieldError::Unresolved { .. }))); } #[sqlx::test] async fn unknown_field_and_type_mismatch_are_rejected(pool: PgPool) { let db = Db::from_pool(pool); let id = setup_object(&db).await; define(&db, "year", FieldType::Integer).await; let unknown = serde_json::json!({ "nope": "x" }); let mut tx = db.pool().begin().await.unwrap(); assert!(matches!( catalog::set_object_fields( &mut tx, AuditActor::System, id, unknown.as_object().unwrap() ) .await, Err(FieldError::UnknownField(_)) )); drop(tx); let wrong = serde_json::json!({ "year": "not a number" }); let mut tx = db.pool().begin().await.unwrap(); assert!(matches!( catalog::set_object_fields(&mut tx, AuditActor::System, id, wrong.as_object().unwrap()) .await, Err(FieldError::TypeMismatch { .. }) )); drop(tx); }