Files
backend_fiesta/services/assistantAudit_test.go
2026-09-23 17:26:13 +05:30

111 lines
3.4 KiB
Go

package services
import (
"context"
"errors"
"testing"
"time"
"nearle/models"
"nearle/repositories"
"nearle/services/tools"
)
type recordingAudit struct {
rows []models.AssistantAudit
err error
}
func (r *recordingAudit) Record(entry models.AssistantAudit) error {
if r.err != nil {
return r.err
}
r.rows = append(r.rows, entry)
return nil
}
func (r *recordingAudit) Recent(int, int) ([]models.AssistantAudit, error) { return r.rows, nil }
func TestAnAuditRowKeepsWhatTheCallActuallyDid(t *testing.T) {
repo := &recordingAudit{}
sink := NewDBAudit(repo)
sink.Write(context.Background(), tools.AuditEntry{
At: time.Now(), Agent: "orders", Tool: "stuck_orders", Scope: "read",
Userid: 904, Tenantid: 1147, Outcome: tools.OutcomeOK, Rows: 3,
Args: map[string]any{"minutes_waiting": 30}, Took: 12 * time.Millisecond,
})
if len(repo.rows) != 1 {
t.Fatalf("wrote %d rows", len(repo.rows))
}
row := repo.rows[0]
if row.Tool != "stuck_orders" || row.Tenantid != 1147 || row.Rows != 3 {
t.Fatalf("the row does not describe the call: %+v", row)
}
if row.Tookms != 12 {
t.Fatalf("duration stored as %d", row.Tookms)
}
}
func TestARefusalIsKept(t *testing.T) {
// The interesting rows. A trail of successes answers "did anything try to
// read another tenant?" with silence, which reads the same as "no".
repo := &recordingAudit{}
NewDBAudit(repo).Write(context.Background(), tools.AuditEntry{
Agent: "orders", Tool: "stuck_orders", Outcome: tools.OutcomeRefused,
Detail: "no tenant on the caller", Userid: 904,
})
if len(repo.rows) != 1 || repo.rows[0].Outcome != tools.OutcomeRefused {
t.Fatalf("a refusal was not recorded: %+v", repo.rows)
}
if repo.rows[0].Detail == "" {
t.Fatal("the refusal does not say why")
}
}
func TestALostAuditRowDoesNotTakeTheAssistantDown(t *testing.T) {
// A full disk must not stop a merchant asking where their orders are. The
// failure is logged; it is not allowed to become an error the caller sees.
repo := &recordingAudit{err: errors.New("disk is full")}
NewDBAudit(repo).Write(context.Background(), tools.AuditEntry{Tool: "stuck_orders"})
// Reaching here without a panic is the assertion.
}
func TestAnAuditSinkWithNoDatabaseStillLogs(t *testing.T) {
// A deployment whose migration has not run yet keeps working.
NewDBAudit(nil).Write(context.Background(), tools.AuditEntry{Tool: "stuck_orders"})
}
func TestArgumentsAreStoredInAStableOrder(t *testing.T) {
// Go randomises map iteration. Without sorting, the same call stores
// different JSON every time and a query looking for one of them finds one
// of them.
args := map[string]any{"zulu": 1, "alpha": "two", "mike": true}
first := repositories.EncodeAuditArgs(args)
for range 20 {
if got := repositories.EncodeAuditArgs(args); got != first {
t.Fatalf("two encodings differ:\n%s\n%s", first, got)
}
}
if first != `{"alpha":"two","mike":true,"zulu":1}` {
t.Fatalf("unexpected encoding: %s", first)
}
}
func TestNoArgumentsIsAnEmptyObjectNotEmptyText(t *testing.T) {
// `""` is not valid jsonb and would fail the insert, losing the row.
if got := repositories.EncodeAuditArgs(nil); got != "{}" {
t.Fatalf("no arguments encoded as %q", got)
}
}
func TestAnUnencodableArgumentDoesNotLoseTheRow(t *testing.T) {
// Losing the whole audit row to save one bad argument is the wrong trade.
got := repositories.EncodeAuditArgs(map[string]any{"bad": make(chan int)})
if got == "" {
t.Fatal("an unencodable argument produced no JSON at all")
}
}