c67b588188
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
3.0 KiB
Rust
106 lines
3.0 KiB
Rust
use db::Db;
|
|
use db::audit;
|
|
use domain::{AuditAction, AuditActor, FieldChange, NewAuditEvent};
|
|
use serde_json::json;
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
fn created(entity_id: Uuid, name: &str) -> NewAuditEvent {
|
|
NewAuditEvent {
|
|
actor: AuditActor::System,
|
|
action: AuditAction::Created,
|
|
entity_type: "object".into(),
|
|
entity_id,
|
|
changes: vec![FieldChange {
|
|
field: "name".into(),
|
|
before: None,
|
|
after: Some(json!(name)),
|
|
}],
|
|
}
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn records_and_reads_back_history_in_order(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
let id = Uuid::new_v4();
|
|
let user = Uuid::new_v4();
|
|
|
|
audit::record(db.pool(), &created(id, "Vase"))
|
|
.await
|
|
.unwrap();
|
|
audit::record(
|
|
db.pool(),
|
|
&NewAuditEvent {
|
|
actor: AuditActor::User(user),
|
|
action: AuditAction::Updated,
|
|
entity_type: "object".into(),
|
|
entity_id: id,
|
|
changes: vec![FieldChange {
|
|
field: "name".into(),
|
|
before: Some(json!("Vase")),
|
|
after: Some(json!("Roman Vase")),
|
|
}],
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let history = audit::history_for(db.pool(), "object", id).await.unwrap();
|
|
assert_eq!(history.len(), 2);
|
|
assert_eq!(history[0].action, AuditAction::Created);
|
|
assert_eq!(history[0].actor, AuditActor::System);
|
|
assert_eq!(history[1].action, AuditAction::Updated);
|
|
assert_eq!(history[1].actor, AuditActor::User(user));
|
|
assert!(history[0].seq < history[1].seq, "ordered by seq");
|
|
assert_eq!(history[1].changes[0].field, "name");
|
|
assert_eq!(history[1].changes[0].after, Some(json!("Roman Vase")));
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn history_is_scoped_to_one_entity(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
let a = Uuid::new_v4();
|
|
let b = Uuid::new_v4();
|
|
audit::record(db.pool(), &created(a, "A")).await.unwrap();
|
|
audit::record(db.pool(), &created(b, "B")).await.unwrap();
|
|
|
|
let only_a = audit::history_for(db.pool(), "object", a).await.unwrap();
|
|
assert_eq!(only_a.len(), 1);
|
|
assert_eq!(only_a[0].entity_id, a);
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn deleted_action_with_empty_changes_round_trips(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
let id = Uuid::new_v4();
|
|
|
|
audit::record(
|
|
db.pool(),
|
|
&NewAuditEvent {
|
|
actor: AuditActor::System,
|
|
action: AuditAction::Deleted,
|
|
entity_type: "object".into(),
|
|
entity_id: id,
|
|
changes: vec![],
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let history = audit::history_for(db.pool(), "object", id).await.unwrap();
|
|
assert_eq!(history.len(), 1);
|
|
assert_eq!(history[0].action, AuditAction::Deleted);
|
|
assert!(history[0].changes.is_empty());
|
|
}
|
|
|
|
#[sqlx::test]
|
|
async fn history_is_empty_for_unknown_entity(pool: PgPool) {
|
|
let db = Db::from_pool(pool);
|
|
|
|
let history = audit::history_for(db.pool(), "object", Uuid::new_v4())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(history.is_empty());
|
|
}
|