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" ); }