111 lines
3.4 KiB
Go
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")
|
|
}
|
|
}
|