test(db): enforce audit_log immutability and transactional atomicity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user