chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
TenantID *string
|
||||
ActorType string
|
||||
ActorPublicID *string
|
||||
Action string
|
||||
ResourceType string
|
||||
ResourceID *string
|
||||
RequestID string
|
||||
Purpose string
|
||||
Result string
|
||||
Detail map[string]any
|
||||
}
|
||||
|
||||
type Repository struct{}
|
||||
|
||||
func NewRepository() *Repository { return &Repository{} }
|
||||
|
||||
func (r *Repository) Record(ctx context.Context, tx pgx.Tx, event Event) error {
|
||||
detail := event.Detail
|
||||
if detail == nil {
|
||||
detail = map[string]any{}
|
||||
}
|
||||
detailJSON, err := json.Marshal(detail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode audit detail: %w", err)
|
||||
}
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO audit_event (
|
||||
tenant_id, actor_type, actor_public_id, action, resource_type,
|
||||
resource_id, request_id, purpose, result, detail
|
||||
) VALUES (
|
||||
$1::uuid, $2, $3::uuid, $4, $5, $6::uuid, NULLIF($7, ''), NULLIF($8, ''), $9, $10::jsonb
|
||||
)`,
|
||||
event.TenantID, event.ActorType, event.ActorPublicID, event.Action,
|
||||
event.ResourceType, event.ResourceID, event.RequestID, event.Purpose,
|
||||
event.Result, detailJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert audit event: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestAuditEventFollowsBusinessTransaction(t *testing.T) {
|
||||
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
||||
if databaseURL == "" {
|
||||
t.Skip("TEST_DATABASE_URL is not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
repository := NewRepository()
|
||||
tenantID := "10000000-0000-0000-0000-000000000001"
|
||||
actorID := "10000000-0000-0000-0000-000000000111"
|
||||
resourceID := "10000000-0000-0000-0000-000000000211"
|
||||
requestID := fmt.Sprintf("audit-integration-%d", time.Now().UnixNano())
|
||||
event := Event{
|
||||
TenantID: &tenantID, ActorType: "user", ActorPublicID: &actorID,
|
||||
Action: "household.read", ResourceType: "household", ResourceID: &resourceID,
|
||||
RequestID: requestID, Purpose: "integration-test", Result: "success",
|
||||
Detail: map[string]any{"synthetic": true},
|
||||
}
|
||||
|
||||
rolledBackTx, err := pool.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repository.Record(ctx, rolledBackTx, event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := rolledBackTx.Rollback(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertAuditCount(t, pool, requestID, 0)
|
||||
|
||||
committedTx, err := pool.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repository.Record(ctx, committedTx, event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := committedTx.Commit(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertAuditCount(t, pool, requestID, 1)
|
||||
if _, err := pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id = $1`, requestID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertAuditCount(t *testing.T, pool *pgxpool.Pool, requestID string, expected int) {
|
||||
t.Helper()
|
||||
var count int
|
||||
if err := pool.QueryRow(context.Background(), `SELECT count(*) FROM audit_event WHERE request_id = $1`, requestID).Scan(&count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count != expected {
|
||||
t.Fatalf("audit count = %d, want %d", count, expected)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user