package store import ( "context" "encoding/json" "path/filepath" "testing" "time" "github.com/oklog/ulid/v2" ) func newAuditTestStore(t *testing.T) (*Store, string) { t.Helper() st, err := Open(context.Background(), filepath.Join(t.TempDir(), "rm.db")) if err != nil { t.Fatalf("open: %v", err) } t.Cleanup(func() { _ = st.Close() }) uid := ulid.Make().String() if err := st.CreateUser(context.Background(), User{ ID: uid, Username: "alice", PasswordHash: "x", Role: RoleOperator, CreatedAt: time.Now().UTC(), }); err != nil { t.Fatalf("create user: %v", err) } return st, uid } func appendAudit(t *testing.T, st *Store, uid, actor, action, targetKind, targetID string, ts time.Time) { t.Helper() var u, tk, ti *string if uid != "" { u = &uid } if targetKind != "" { tk = &targetKind } if targetID != "" { ti = &targetID } if err := st.AppendAudit(context.Background(), AuditEntry{ ID: ulid.Make().String(), UserID: u, Actor: actor, Action: action, TargetKind: tk, TargetID: ti, TS: ts, Payload: json.RawMessage(`{}`), }); err != nil { t.Fatalf("append: %v", err) } } func TestListAuditFiltersAndOrdering(t *testing.T) { t.Parallel() st, uid := newAuditTestStore(t) t0 := time.Now().UTC() appendAudit(t, st, uid, "user", "host.enrolled", "host", "h1", t0.Add(-3*time.Hour)) appendAudit(t, st, uid, "user", "alert.acknowledge", "alert", "a1", t0.Add(-2*time.Hour)) appendAudit(t, st, uid, "user", "alert.resolve", "alert", "a1", t0.Add(-time.Hour)) appendAudit(t, st, "", "system", "host.auto_init", "host", "h1", t0.Add(-30*time.Minute)) all, err := st.ListAudit(context.Background(), AuditFilter{}) if err != nil { t.Fatalf("list: %v", err) } if len(all) != 4 { t.Fatalf("len: got %d want 4", len(all)) } // Ordered ts DESC — most recent first. if all[0].Action != "host.auto_init" || all[3].Action != "host.enrolled" { t.Errorf("ordering: got %s ... %s", all[0].Action, all[3].Action) } // Action prefix filter: alert.* → 2 rows. got, err := st.ListAudit(context.Background(), AuditFilter{ActionLike: "alert."}) if err != nil { t.Fatalf("filter alert.: %v", err) } if len(got) != 2 { t.Errorf("alert.* filter: got %d want 2", len(got)) } // User filter excludes system rows. got, _ = st.ListAudit(context.Background(), AuditFilter{UserID: uid}) if len(got) != 3 { t.Errorf("user filter: got %d want 3", len(got)) } // Actor=system isolates the auto_init. got, _ = st.ListAudit(context.Background(), AuditFilter{Actor: "system"}) if len(got) != 1 || got[0].Action != "host.auto_init" { t.Errorf("actor=system: got %+v", got) } // Target kind filter. got, _ = st.ListAudit(context.Background(), AuditFilter{TargetKind: "alert"}) if len(got) != 2 { t.Errorf("target_kind=alert: got %d want 2", len(got)) } // Time range: last 90m → resolve + auto_init. got, _ = st.ListAudit(context.Background(), AuditFilter{Since: t0.Add(-90 * time.Minute)}) if len(got) != 2 { t.Errorf("since 90m: got %d want 2", len(got)) } // Limit clamps result count. got, _ = st.ListAudit(context.Background(), AuditFilter{Limit: 2}) if len(got) != 2 { t.Errorf("limit: got %d want 2", len(got)) } } func TestDistinctAuditActions(t *testing.T) { t.Parallel() st, uid := newAuditTestStore(t) t0 := time.Now().UTC() appendAudit(t, st, uid, "user", "host.enrolled", "host", "h1", t0) appendAudit(t, st, uid, "user", "host.enrolled", "host", "h2", t0) appendAudit(t, st, uid, "user", "alert.acknowledge", "alert", "a1", t0) got, err := st.DistinctAuditActions(context.Background()) if err != nil { t.Fatalf("distinct: %v", err) } want := []string{"alert.acknowledge", "host.enrolled"} if len(got) != len(want) { t.Fatalf("got %v want %v", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("[%d]: got %q want %q", i, got[i], want[i]) } } }