Files
Jay Brown 17fc813823 Merged in feature/mutable-metadata1 (pull request #221)
M1, M2 and M3 complete

* M1, M2 and M3 complete

* review changes

* docs

* docs
2026-04-16 23:11:26 +00:00

142 lines
4.5 KiB
Go

package customschema_test
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"queryorchestration/internal/customschema"
)
// newCapturingLogger returns a *slog.Logger that writes JSON lines into
// the returned buffer. Tests parse the buffer to assert emitted fields.
func newCapturingLogger() (*slog.Logger, *bytes.Buffer) {
buf := &bytes.Buffer{}
handler := slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo})
return slog.New(handler), buf
}
// decodeSingleLine parses exactly one JSON line from the buffer into a
// map. It fails the test if the buffer is empty or contains more than
// one line, because the audit sink contract is one-event-per-Record.
func decodeSingleLine(t *testing.T, buf *bytes.Buffer) map[string]any {
t.Helper()
raw := bytes.TrimSpace(buf.Bytes())
require.NotEmpty(t, raw, "expected sink to emit a log line, got empty buffer")
lines := bytes.Split(raw, []byte("\n"))
require.Len(t, lines, 1, "expected exactly one emitted line, got %d", len(lines))
out := map[string]any{}
require.NoError(t, json.Unmarshal(lines[0], &out))
return out
}
// TestSlogAuditSink_Record locks in the contract for the default audit
// sink that wraps a *slog.Logger. These assertions are the strict
// specification the engineer must satisfy in the next dispatch.
//
// Expected slog event:
//
// level = INFO
// msg = "audit"
// attrs = audit=true, action, actor, resource_id, client_id,
// timestamp (RFC3339Nano), plus every Details key merged flat.
func TestSlogAuditSink_Record(t *testing.T) {
actor := "00000000-0000-0000-0000-0000000000aa"
resourceID := uuid.MustParse("11111111-1111-1111-1111-111111111111")
clientID := "CLIENT_AUDIT_TEST"
ts := time.Date(2026, 4, 14, 12, 34, 56, 789000000, time.UTC)
buildRec := func(details map[string]any) customschema.AuditRecord {
return customschema.AuditRecord{
Actor: actor,
Action: customschema.AuditActionSchemaCreate,
ResourceID: resourceID,
ClientID: clientID,
Timestamp: ts,
Details: details,
}
}
t.Run("emits_audit_true_attribute", func(t *testing.T) {
logger, buf := newCapturingLogger()
sink := customschema.NewSlogAuditSink(logger)
err := sink.Record(t.Context(), buildRec(map[string]any{
"name": "alpha",
"version": 1,
}))
require.NoError(t, err)
event := decodeSingleLine(t, buf)
// slog JSON encodes booleans as literal true/false, but accept
// the string "true" as well in case a handler variant stringifies.
auditVal, ok := event["audit"]
require.True(t, ok, "missing audit attribute")
switch v := auditVal.(type) {
case bool:
require.True(t, v, "audit attribute must be true")
case string:
require.Equal(t, "true", v, "audit attribute must be true")
default:
t.Fatalf("audit attribute has unexpected type %T", v)
}
require.Equal(t, "schema.create", event["action"])
require.Equal(t, actor, event["actor"])
require.Equal(t, resourceID.String(), event["resource_id"])
require.Equal(t, clientID, event["client_id"])
tsAttr, ok := event["timestamp"].(string)
require.True(t, ok, "timestamp must be a string")
require.NotEmpty(t, tsAttr, "timestamp must not be empty")
require.Equal(t, "alpha", event["name"], "Details.name must be merged flat")
// json.Unmarshal decodes numbers as float64.
require.EqualValues(t, 1, event["version"], "Details.version must be merged flat")
require.Equal(t, "INFO", event["level"])
require.Equal(t, "audit", event["msg"])
})
t.Run("record_returns_nil_on_success", func(t *testing.T) {
logger, _ := newCapturingLogger()
sink := customschema.NewSlogAuditSink(logger)
err := sink.Record(t.Context(), buildRec(map[string]any{"k": "v"}))
require.NoError(t, err)
})
t.Run("empty_details_accepted", func(t *testing.T) {
logger, buf := newCapturingLogger()
sink := customschema.NewSlogAuditSink(logger)
require.NotPanics(t, func() {
err := sink.Record(t.Context(), buildRec(nil))
require.NoError(t, err)
})
event := decodeSingleLine(t, buf)
_, hasAudit := event["audit"]
require.True(t, hasAudit, "audit attribute must be present even with nil Details")
require.Equal(t, "schema.create", event["action"])
})
t.Run("context_cancellation_does_not_block_emission", func(t *testing.T) {
logger, _ := newCapturingLogger()
sink := customschema.NewSlogAuditSink(logger)
ctx, cancel := context.WithCancel(t.Context())
cancel()
err := sink.Record(ctx, buildRec(map[string]any{"k": "v"}))
require.NoError(t, err)
})
}