felt/internal/audit/trail_test.go
Mikkel Georgsen 1978d3d421 docs(01-05): complete Blind Structure + Chip Sets + Templates plan
- SUMMARY.md with self-check passed
- STATE.md updated: plan 6 of 14, 36% progress, 4 decisions added
- ROADMAP.md updated: 5/14 plans complete
- REQUIREMENTS.md: 15 requirements marked complete (BLIND-01-06, CHIP-01-04, FIN-01,02,05,06,10)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 04:02:11 +01:00

349 lines
8.8 KiB
Go

package audit
import (
"context"
"encoding/json"
"testing"
"github.com/felt-app/felt/internal/store"
)
// mockPublisher records published audit events for testing.
type mockPublisher struct {
published []struct {
TournamentID string
Data []byte
}
}
func (m *mockPublisher) PublishAudit(ctx context.Context, tournamentID string, data []byte) error {
m.published = append(m.published, struct {
TournamentID string
Data []byte
}{tournamentID, data})
return nil
}
func setupTestTrail(t *testing.T) (*Trail, *mockPublisher) {
t.Helper()
tmpDir := t.TempDir()
db, err := store.Open(tmpDir, true)
if err != nil {
t.Fatalf("open database: %v", err)
}
t.Cleanup(func() { db.Close() })
pub := &mockPublisher{}
trail := NewTrail(db.DB, pub)
return trail, pub
}
func TestRecordAuditEntry(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
tournamentID := "550e8400-e29b-41d4-a716-446655440000"
entry := AuditEntry{
TournamentID: &tournamentID,
OperatorID: "op-1",
Action: ActionPlayerBuyin,
TargetType: "player",
TargetID: "player-1",
PreviousState: json.RawMessage(`{"chips":0}`),
NewState: json.RawMessage(`{"chips":10000}`),
}
recorded, err := trail.Record(ctx, entry)
if err != nil {
t.Fatalf("record entry: %v", err)
}
if recorded.ID == "" {
t.Fatal("expected generated ID")
}
if recorded.Timestamp == 0 {
t.Fatal("expected generated timestamp")
}
if recorded.Action != ActionPlayerBuyin {
t.Fatalf("expected action %s, got %s", ActionPlayerBuyin, recorded.Action)
}
}
func TestRecordPersistsToDatabase(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
tournamentID := "550e8400-e29b-41d4-a716-446655440001"
entry := AuditEntry{
TournamentID: &tournamentID,
OperatorID: "op-1",
Action: ActionPlayerBust,
TargetType: "player",
TargetID: "player-2",
PreviousState: json.RawMessage(`{"status":"active","chips":5000}`),
NewState: json.RawMessage(`{"status":"busted","chips":0}`),
}
recorded, err := trail.Record(ctx, entry)
if err != nil {
t.Fatalf("record entry: %v", err)
}
// Retrieve and verify
fetched, err := trail.GetEntry(ctx, recorded.ID)
if err != nil {
t.Fatalf("get entry: %v", err)
}
if fetched.Action != ActionPlayerBust {
t.Fatalf("expected action %s, got %s", ActionPlayerBust, fetched.Action)
}
if fetched.OperatorID != "op-1" {
t.Fatalf("expected operator_id op-1, got %s", fetched.OperatorID)
}
if fetched.TargetType != "player" {
t.Fatalf("expected target_type player, got %s", fetched.TargetType)
}
if fetched.TargetID != "player-2" {
t.Fatalf("expected target_id player-2, got %s", fetched.TargetID)
}
if string(fetched.PreviousState) != `{"status":"active","chips":5000}` {
t.Fatalf("unexpected previous_state: %s", fetched.PreviousState)
}
if string(fetched.NewState) != `{"status":"busted","chips":0}` {
t.Fatalf("unexpected new_state: %s", fetched.NewState)
}
}
func TestRecordPublishesToNATS(t *testing.T) {
trail, pub := setupTestTrail(t)
ctx := context.Background()
tournamentID := "550e8400-e29b-41d4-a716-446655440002"
entry := AuditEntry{
TournamentID: &tournamentID,
OperatorID: "op-1",
Action: ActionClockStart,
TargetType: "tournament",
TargetID: tournamentID,
}
_, err := trail.Record(ctx, entry)
if err != nil {
t.Fatalf("record entry: %v", err)
}
if len(pub.published) != 1 {
t.Fatalf("expected 1 published message, got %d", len(pub.published))
}
if pub.published[0].TournamentID != tournamentID {
t.Fatalf("expected tournament ID %s, got %s", tournamentID, pub.published[0].TournamentID)
}
// Verify the published data is valid JSON
var publishedEntry AuditEntry
if err := json.Unmarshal(pub.published[0].Data, &publishedEntry); err != nil {
t.Fatalf("unmarshal published data: %v", err)
}
if publishedEntry.Action != ActionClockStart {
t.Fatalf("expected action %s in NATS message, got %s", ActionClockStart, publishedEntry.Action)
}
}
func TestRecordWithoutTournamentIDSkipsNATS(t *testing.T) {
trail, pub := setupTestTrail(t)
ctx := context.Background()
// Venue-level action (no tournament ID)
entry := AuditEntry{
OperatorID: "op-1",
Action: ActionOperatorLogin,
TargetType: "operator",
TargetID: "op-1",
}
_, err := trail.Record(ctx, entry)
if err != nil {
t.Fatalf("record entry: %v", err)
}
if len(pub.published) != 0 {
t.Fatalf("expected no published messages for venue-level action, got %d", len(pub.published))
}
}
func TestRecordRequiresAction(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
entry := AuditEntry{
OperatorID: "op-1",
TargetType: "player",
TargetID: "player-1",
}
_, err := trail.Record(ctx, entry)
if err == nil {
t.Fatal("expected error for missing action")
}
}
func TestRecordDefaultsOperatorToSystem(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
entry := AuditEntry{
Action: ActionTournamentCreate,
TargetType: "tournament",
TargetID: "t-1",
}
recorded, err := trail.Record(ctx, entry)
if err != nil {
t.Fatalf("record entry: %v", err)
}
if recorded.OperatorID != "system" {
t.Fatalf("expected operator_id 'system', got %s", recorded.OperatorID)
}
}
func TestGetEntries(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
tournamentID := "550e8400-e29b-41d4-a716-446655440003"
// Record multiple entries
for i := 0; i < 5; i++ {
_, err := trail.Record(ctx, AuditEntry{
TournamentID: &tournamentID,
OperatorID: "op-1",
Action: ActionPlayerBuyin,
TargetType: "player",
TargetID: "player-1",
})
if err != nil {
t.Fatalf("record entry %d: %v", i, err)
}
}
// Get all entries for tournament
entries, err := trail.GetEntries(ctx, tournamentID, 10, 0)
if err != nil {
t.Fatalf("get entries: %v", err)
}
if len(entries) != 5 {
t.Fatalf("expected 5 entries, got %d", len(entries))
}
// Test pagination
page1, err := trail.GetEntries(ctx, tournamentID, 3, 0)
if err != nil {
t.Fatalf("get page 1: %v", err)
}
if len(page1) != 3 {
t.Fatalf("expected 3 entries on page 1, got %d", len(page1))
}
page2, err := trail.GetEntries(ctx, tournamentID, 3, 3)
if err != nil {
t.Fatalf("get page 2: %v", err)
}
if len(page2) != 2 {
t.Fatalf("expected 2 entries on page 2, got %d", len(page2))
}
}
func TestGetEntriesFiltersByTournament(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
t1 := "550e8400-e29b-41d4-a716-446655440010"
t2 := "550e8400-e29b-41d4-a716-446655440011"
// Record entries for two tournaments
for i := 0; i < 3; i++ {
trail.Record(ctx, AuditEntry{
TournamentID: &t1,
OperatorID: "op-1",
Action: ActionPlayerBuyin,
TargetType: "player",
TargetID: "player-1",
})
}
for i := 0; i < 2; i++ {
trail.Record(ctx, AuditEntry{
TournamentID: &t2,
OperatorID: "op-1",
Action: ActionPlayerBuyin,
TargetType: "player",
TargetID: "player-2",
})
}
// Filter by tournament 1
entries, err := trail.GetEntries(ctx, t1, 50, 0)
if err != nil {
t.Fatalf("get entries: %v", err)
}
if len(entries) != 3 {
t.Fatalf("expected 3 entries for tournament 1, got %d", len(entries))
}
// Filter by tournament 2
entries, err = trail.GetEntries(ctx, t2, 50, 0)
if err != nil {
t.Fatalf("get entries: %v", err)
}
if len(entries) != 2 {
t.Fatalf("expected 2 entries for tournament 2, got %d", len(entries))
}
}
func TestGetEntriesByAction(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
// Record different action types
trail.Record(ctx, AuditEntry{OperatorID: "op-1", Action: ActionPlayerBuyin, TargetType: "player", TargetID: "p-1"})
trail.Record(ctx, AuditEntry{OperatorID: "op-1", Action: ActionPlayerBust, TargetType: "player", TargetID: "p-1"})
trail.Record(ctx, AuditEntry{OperatorID: "op-1", Action: ActionPlayerBuyin, TargetType: "player", TargetID: "p-2"})
entries, err := trail.GetEntriesByAction(ctx, ActionPlayerBuyin, 50, 0)
if err != nil {
t.Fatalf("get entries by action: %v", err)
}
if len(entries) != 2 {
t.Fatalf("expected 2 buyin entries, got %d", len(entries))
}
}
func TestRecorderFunc(t *testing.T) {
trail, _ := setupTestTrail(t)
ctx := context.Background()
recorder := trail.RecorderFunc()
err := recorder(ctx, ActionOperatorLogin, "operator", "op-1", map[string]interface{}{
"operator_name": "Admin",
"role": "admin",
})
if err != nil {
t.Fatalf("recorder func: %v", err)
}
// Verify entry was persisted
entries, err := trail.GetEntriesByAction(ctx, ActionOperatorLogin, 50, 0)
if err != nil {
t.Fatalf("get entries: %v", err)
}
if len(entries) != 1 {
t.Fatalf("expected 1 login entry, got %d", len(entries))
}
if entries[0].TargetType != "operator" {
t.Fatalf("expected target_type operator, got %s", entries[0].TargetType)
}
}