From 45aea6b7025e72fa88f73cf77246dd39ad9dd7d3 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 2 Jun 2026 07:58:10 +0200 Subject: [PATCH] test(db): enforce audit_log immutability and transactional atomicity Co-Authored-By: Claude Sonnet 4.6 --- crates/db/tests/audit_immutability.rs | 86 +++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/db/tests/audit_immutability.rs diff --git a/crates/db/tests/audit_immutability.rs b/crates/db/tests/audit_immutability.rs new file mode 100644 index 0000000..3cf66ef --- /dev/null +++ b/crates/db/tests/audit_immutability.rs @@ -0,0 +1,86 @@ +use db::Db; +use db::audit; +use domain::{AuditAction, AuditActor, NewAuditEvent}; +use sqlx::PgPool; +use uuid::Uuid; + +fn sample() -> NewAuditEvent { + NewAuditEvent { + actor: AuditActor::System, + action: AuditAction::Created, + entity_type: "object".into(), + entity_id: Uuid::new_v4(), + changes: vec![], + } +} + +async fn count(pool: &PgPool) -> i64 { + sqlx::query_scalar("SELECT count(*) FROM audit_log") + .fetch_one(pool) + .await + .unwrap() +} + +#[sqlx::test] +async fn update_delete_truncate_are_rejected(pool: PgPool) { + let db = Db::from_pool(pool); + + audit::record(db.pool(), &sample()).await.unwrap(); + + // Each failing statement poisons its connection (Postgres enters aborted-transaction + // state). Acquire a fresh connection per statement so later assertions are independent. + let updated = sqlx::query("UPDATE audit_log SET action = 'deleted'") + .execute(db.pool()) + .await; + + assert!(updated.is_err(), "UPDATE must be rejected by the trigger"); + + let deleted = sqlx::query("DELETE FROM audit_log") + .execute(db.pool()) + .await; + + assert!(deleted.is_err(), "DELETE must be rejected by the trigger"); + + let truncated = sqlx::query("TRUNCATE audit_log").execute(db.pool()).await; + + assert!( + truncated.is_err(), + "TRUNCATE must be rejected by the trigger" + ); + + assert_eq!(count(db.pool()).await, 1, "the row is still there"); +} + +#[sqlx::test] +async fn record_rolls_back_with_caller_transaction(pool: PgPool) { + let db = Db::from_pool(pool); + + let mut tx = db.pool().begin().await.unwrap(); + + audit::record(&mut *tx, &sample()).await.unwrap(); + + tx.rollback().await.unwrap(); + + assert_eq!( + count(db.pool()).await, + 0, + "a rolled-back audit record must not persist" + ); +} + +#[sqlx::test] +async fn record_commits_with_caller_transaction(pool: PgPool) { + let db = Db::from_pool(pool); + + let mut tx = db.pool().begin().await.unwrap(); + + audit::record(&mut *tx, &sample()).await.unwrap(); + + tx.commit().await.unwrap(); + + assert_eq!( + count(db.pool()).await, + 1, + "a committed audit record persists" + ); +}