60a1b8dccf
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
268 lines
7.6 KiB
Rust
268 lines
7.6 KiB
Rust
use db::{Db, audit, catalog};
|
|
use domain::{AuditAction, AuditActor, ObjectInput, Visibility};
|
|
use sqlx::PgPool;
|
|
|
|
fn sample_input(number: &str) -> ObjectInput {
|
|
ObjectInput {
|
|
object_number: number.into(),
|
|
object_name: "vase".into(),
|
|
number_of_objects: 1,
|
|
brief_description: Some("a small vase".into()),
|
|
current_location: Some("shelf A1".into()),
|
|
current_owner: None,
|
|
recorder: Some("anna".into()),
|
|
recording_date: None,
|
|
visibility: Visibility::Draft,
|
|
}
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn create_reads_back_and_audits(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
|
|
let mut tx = db.pool().begin().await.unwrap();
|
|
let id = catalog::create_object(&mut tx, AuditActor::System, &sample_input("LM-1"))
|
|
.await
|
|
.unwrap();
|
|
tx.commit().await.unwrap();
|
|
|
|
let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
|
|
assert_eq!(obj.object_number, "LM-1");
|
|
assert_eq!(obj.object_name, "vase");
|
|
assert_eq!(obj.number_of_objects, 1);
|
|
assert_eq!(obj.brief_description.as_deref(), Some("a small vase"));
|
|
assert_eq!(obj.visibility, Visibility::Draft);
|
|
|
|
let history = audit::history_for(db.pool(), "object", id.to_uuid())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(history.len(), 1);
|
|
assert_eq!(history[0].action, AuditAction::Created);
|
|
assert_eq!(history[0].actor, AuditActor::System);
|
|
assert!(
|
|
history[0]
|
|
.changes
|
|
.iter()
|
|
.any(|c| c.field == "object_number")
|
|
);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn list_returns_created_objects(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
let mut tx = db.pool().begin().await.unwrap();
|
|
catalog::create_object(&mut tx, AuditActor::System, &sample_input("LM-1"))
|
|
.await
|
|
.unwrap();
|
|
catalog::create_object(&mut tx, AuditActor::System, &sample_input("LM-2"))
|
|
.await
|
|
.unwrap();
|
|
tx.commit().await.unwrap();
|
|
|
|
let all = catalog::list_objects(db.pool()).await.unwrap();
|
|
assert_eq!(all.len(), 2);
|
|
assert_eq!(all[0].object_number, "LM-1");
|
|
assert_eq!(all[1].object_number, "LM-2");
|
|
}
|
|
|
|
fn input(number: &str, name: &str, visibility: Visibility) -> ObjectInput {
|
|
ObjectInput {
|
|
object_number: number.into(),
|
|
object_name: name.into(),
|
|
number_of_objects: 1,
|
|
brief_description: None,
|
|
current_location: None,
|
|
current_owner: None,
|
|
recorder: None,
|
|
recording_date: None,
|
|
visibility,
|
|
}
|
|
}
|
|
|
|
async fn seed(pool: &PgPool, inputs: &[ObjectInput]) {
|
|
let db = Db::from_pool(pool.clone());
|
|
let mut tx = db.pool().begin().await.unwrap();
|
|
|
|
for it in inputs {
|
|
catalog::create_object(&mut tx, AuditActor::System, it)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
tx.commit().await.unwrap();
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn query_orders_by_name_descending(pool: PgPool) {
|
|
let db = Db::from_pool(pool.clone());
|
|
|
|
seed(
|
|
&pool,
|
|
&[
|
|
input("LM-1", "alpha", Visibility::Draft),
|
|
input("LM-2", "gamma", Visibility::Draft),
|
|
input("LM-3", "beta", Visibility::Draft),
|
|
],
|
|
)
|
|
.await;
|
|
|
|
let query = catalog::ObjectQuery {
|
|
sort: catalog::ObjectSort::ObjectName,
|
|
descending: true,
|
|
visibility: None,
|
|
q: None,
|
|
};
|
|
|
|
let rows = catalog::list_objects_query(db.pool(), &query, 50, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
let names: Vec<&str> = rows.iter().map(|o| o.object_name.as_str()).collect();
|
|
assert_eq!(names, ["gamma", "beta", "alpha"]);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn query_filters_by_visibility(pool: PgPool) {
|
|
let db = Db::from_pool(pool.clone());
|
|
|
|
seed(
|
|
&pool,
|
|
&[
|
|
input("LM-1", "draft one", Visibility::Draft),
|
|
input("LM-2", "internal one", Visibility::Internal),
|
|
input("LM-3", "draft two", Visibility::Draft),
|
|
],
|
|
)
|
|
.await;
|
|
|
|
let query = catalog::ObjectQuery {
|
|
sort: catalog::ObjectSort::ObjectNumber,
|
|
descending: false,
|
|
visibility: Some("draft"),
|
|
q: None,
|
|
};
|
|
|
|
let rows = catalog::list_objects_query(db.pool(), &query, 50, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(rows.len(), 2);
|
|
assert!(rows.iter().all(|o| o.visibility == Visibility::Draft));
|
|
|
|
let total = catalog::count_objects_query(db.pool(), Some("draft"), None)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(total, 2);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn query_quick_filter_matches_number_or_name(pool: PgPool) {
|
|
let db = Db::from_pool(pool.clone());
|
|
|
|
seed(
|
|
&pool,
|
|
&[
|
|
input("RED-1", "scarlet vase", Visibility::Draft),
|
|
input("BLU-1", "azure bowl", Visibility::Draft),
|
|
input("LM-9", "red kettle", Visibility::Internal),
|
|
],
|
|
)
|
|
.await;
|
|
|
|
// Matches the object_number of the first row.
|
|
let by_number = catalog::ObjectQuery {
|
|
sort: catalog::ObjectSort::ObjectNumber,
|
|
descending: false,
|
|
visibility: None,
|
|
q: Some("red"),
|
|
};
|
|
let rows = catalog::list_objects_query(db.pool(), &by_number, 50, 0)
|
|
.await
|
|
.unwrap();
|
|
// ILIKE: "RED-1" by number and "red kettle" by name.
|
|
assert_eq!(rows.len(), 2);
|
|
|
|
let total = catalog::count_objects_query(db.pool(), None, Some("red"))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(total, 2);
|
|
|
|
// A term matching only a name.
|
|
let by_name = catalog::ObjectQuery {
|
|
sort: catalog::ObjectSort::ObjectNumber,
|
|
descending: false,
|
|
visibility: None,
|
|
q: Some("azure"),
|
|
};
|
|
let rows = catalog::list_objects_query(db.pool(), &by_name, 50, 0)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(rows.len(), 1);
|
|
assert_eq!(rows[0].object_number, "BLU-1");
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn object_by_id_missing_is_none(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
assert!(
|
|
catalog::object_by_id(db.pool(), domain::ObjectId::new())
|
|
.await
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn object_with_date_and_all_none_optionals_round_trips(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
let date = time::Date::from_calendar_date(2020, time::Month::January, 28).unwrap();
|
|
let input = ObjectInput {
|
|
object_number: "LM-3".into(),
|
|
object_name: "drawing".into(),
|
|
number_of_objects: 1,
|
|
brief_description: None,
|
|
current_location: None,
|
|
current_owner: None,
|
|
recorder: None,
|
|
recording_date: Some(date),
|
|
visibility: Visibility::Internal,
|
|
};
|
|
|
|
let mut tx = db.pool().begin().await.unwrap();
|
|
let id = catalog::create_object(&mut tx, AuditActor::System, &input)
|
|
.await
|
|
.unwrap();
|
|
tx.commit().await.unwrap();
|
|
|
|
let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
|
|
assert_eq!(obj.recording_date, Some(date));
|
|
assert_eq!(obj.brief_description, None);
|
|
assert_eq!(obj.current_location, None);
|
|
assert_eq!(obj.current_owner, None);
|
|
assert_eq!(obj.recorder, None);
|
|
assert_eq!(obj.visibility, Visibility::Internal);
|
|
|
|
let history = audit::history_for(db.pool(), "object", id.to_uuid())
|
|
.await
|
|
.unwrap();
|
|
assert!(
|
|
history[0]
|
|
.changes
|
|
.iter()
|
|
.any(|c| c.field == "recording_date")
|
|
);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn new_object_has_empty_fields(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
let mut tx = db.pool().begin().await.unwrap();
|
|
let id = catalog::create_object(&mut tx, AuditActor::System, &sample_input("LM-9"))
|
|
.await
|
|
.unwrap();
|
|
tx.commit().await.unwrap();
|
|
|
|
let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
|
|
assert_eq!(obj.fields, serde_json::json!({}));
|
|
}
|