Documentation
¶
Overview ¶
Package audittest provides test helpers for consumers of the audit library. It provides an in-memory Recorder that captures audit events for assertion, a MetricsRecorder that captures all metrics calls, and convenience constructors that eliminate test boilerplate.
The test auditor is a fully functional audit.Auditor — same validation, same taxonomy enforcement — but events land in memory instead of being written to a file, syslog, or webhook.
Both New and NewQuick default to synchronous delivery: events are available in the Recorder immediately after audit.Auditor.AuditEvent returns. No Close-before-assert ceremony is needed. Use WithAsync to opt into asynchronous delivery for tests that exercise drain timeout or buffer backpressure.
Quick Start ¶
func TestMyHandler(t *testing.T) {
auditor, events, metrics := audittest.New(t, taxonomyYAML)
myHandler(auditor) // code under test
// Assert immediately — synchronous delivery means events are already available.
require.Equal(t, 1, events.Count())
assert.Equal(t, "user_create", events.Events()[0].EventType)
assert.Equal(t, 1, metrics.EventDeliveries("recorder", "success"))
}
Table-Driven Tests ¶
Use Recorder.Reset to clear captured events between sub-tests without creating a new auditor:
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
events.Reset()
svc.Do(tc.action)
// assert on events for this sub-test only
})
}
Index ¶
- func CNMismatchCert(tb testing.TB) (certPath, keyPath, caPath string)
- func ExpiredCert(tb testing.TB) (certPath, keyPath, caPath string)
- func New(tb testing.TB, taxonomyYAML []byte, opts ...Option) (*audit.Auditor, *Recorder, *MetricsRecorder)
- func NewQuick(tb testing.TB, eventTypes ...string) (*audit.Auditor, *Recorder, *MetricsRecorder)
- func QuickTaxonomy(eventTypes ...string) *audit.Taxonomy
- func WriteKeyPEM(tb testing.TB, path string, key *ecdsa.PrivateKey)
- func WritePEM(tb testing.TB, path, blockType string, data []byte)
- type BadCerts
- type MetricsRecorder
- func (m *MetricsRecorder) BufferDrops() int
- func (m *MetricsRecorder) EventDeliveries(output string, status audit.EventStatus) int
- func (m *MetricsRecorder) FileRotations(path string) int
- func (m *MetricsRecorder) FilteredCount(eventType string) int
- func (m *MetricsRecorder) OutputErrors(output string) int
- func (m *MetricsRecorder) OutputFiltered(output string) int
- func (m *MetricsRecorder) RecordBufferDrop()
- func (m *MetricsRecorder) RecordDelivery(output string, status audit.EventStatus)
- func (m *MetricsRecorder) RecordFiltered(eventType string)
- func (m *MetricsRecorder) RecordOutputError(output string)
- func (m *MetricsRecorder) RecordOutputFiltered(output string)
- func (m *MetricsRecorder) RecordQueueDepth(_, _ int)
- func (m *MetricsRecorder) RecordReconnect(address string, success bool)
- func (m *MetricsRecorder) RecordRotation(path string)
- func (m *MetricsRecorder) RecordSerializationError(eventType string)
- func (m *MetricsRecorder) RecordSubmitted()
- func (m *MetricsRecorder) RecordValidationError(eventType string)
- func (m *MetricsRecorder) Reset()
- func (m *MetricsRecorder) SerializationErrors(eventType string) int
- func (m *MetricsRecorder) SubmittedCount() int
- func (m *MetricsRecorder) SyslogReconnects(address string, success bool) int
- func (m *MetricsRecorder) ValidationErrors(eventType string) int
- type Option
- type OutputMetricsRecorder
- func (m *OutputMetricsRecorder) DropCount() int
- func (m *OutputMetricsRecorder) ErrorCount() int
- func (m *OutputMetricsRecorder) FlushCount() int
- func (m *OutputMetricsRecorder) RecordDrop()
- func (m *OutputMetricsRecorder) RecordError()
- func (m *OutputMetricsRecorder) RecordFlush(_ int, d time.Duration)
- func (m *OutputMetricsRecorder) RecordQueueDepth(_, _ int)
- func (m *OutputMetricsRecorder) RecordRetry(_ int)
- func (m *OutputMetricsRecorder) Reset()
- func (m *OutputMetricsRecorder) RetryCount() int
- type RecordedEvent
- func (e RecordedEvent) BoolField(key string) bool
- func (e RecordedEvent) Field(key string) any
- func (e RecordedEvent) FloatField(key string) float64
- func (e RecordedEvent) GoString() string
- func (e RecordedEvent) HasField(key string, want any) bool
- func (e RecordedEvent) IntField(key string) int
- func (e RecordedEvent) StringField(key string) string
- func (e RecordedEvent) UserFields() map[string]any
- type Recorder
- func (r *Recorder) AssertContains(tb testing.TB, eventType string, fields audit.Fields)
- func (r *Recorder) Close() error
- func (r *Recorder) Count() int
- func (r *Recorder) Events() []RecordedEvent
- func (r *Recorder) FindByField(key string, val any) []RecordedEvent
- func (r *Recorder) FindByType(eventType string) []RecordedEvent
- func (r *Recorder) First() (RecordedEvent, bool)
- func (r *Recorder) GoString() string
- func (r *Recorder) Last() (RecordedEvent, bool)
- func (r *Recorder) Name() string
- func (r *Recorder) RequireEmpty(tb testing.TB)
- func (r *Recorder) RequireEvent(tb testing.TB, eventType string) RecordedEvent
- func (r *Recorder) RequireEvents(tb testing.TB, n int) []RecordedEvent
- func (r *Recorder) Reset()
- func (r *Recorder) WaitForN(tb testing.TB, n int, timeout time.Duration) bool
- func (r *Recorder) Write(data []byte) error
- type TestCerts
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CNMismatchCert ¶ added in v0.1.12
CNMismatchCert is the matching convenience wrapper for the CN/SAN-mismatch cert pair. Returns (BadCerts.CNMismatchCertPath, .CNMismatchKeyPath, .CAPath).
func ExpiredCert ¶ added in v0.1.12
ExpiredCert is a convenience wrapper that returns just the expired-NotAfter cert pair (plus its CA path). Equivalent to (BadCerts.ExpiredCertPath, .ExpiredKeyPath, .CAPath) from GenerateBadCerts.
func New ¶ added in v0.1.10
func New(tb testing.TB, taxonomyYAML []byte, opts ...Option) (*audit.Auditor, *Recorder, *MetricsRecorder)
New creates a test auditor with an in-memory Recorder and MetricsRecorder. The taxonomy is parsed from YAML bytes.
The auditor defaults to synchronous delivery — events are available in the Recorder immediately after audit.Auditor.AuditEvent returns, with no Close-before-assert ceremony needed. Use WithAsync to opt into asynchronous delivery for tests that exercise drain timeout or buffer backpressure.
QueueSize defaults to 100 (intentionally small — the in-memory recorder has no I/O cost). tb.Cleanup is registered to call auditor.Close() as a safety net against goroutine leaks.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/axonops/audit"
"github.com/axonops/audit/audittest"
)
var exampleTaxonomyYAML = []byte(`
version: 1
categories:
write:
- user_create
events:
user_create:
fields:
outcome: {required: true}
actor_id: {required: true}
`)
func main() {
// Use a real *testing.T in actual tests — this is for runnable doc only.
t := &testing.T{}
auditor, events, metrics := audittest.New(t, exampleTaxonomyYAML)
_ = auditor.AuditEvent(audit.NewEvent("user_create", audit.Fields{
"outcome": "success",
"actor_id": "alice",
}))
fmt.Println("count:", events.Count())
fmt.Println("type:", events.Events()[0].EventType)
fmt.Println("deliveries:", metrics.EventDeliveries("recorder", "success"))
}
Output: count: 1 type: user_create deliveries: 1
func NewQuick ¶ added in v0.1.10
NewQuick creates a test auditor with a permissive taxonomy containing the named event types. No required fields, no unknown field validation. Defaults to synchronous delivery — events are available in the Recorder immediately without calling Close.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/axonops/audit"
"github.com/axonops/audit/audittest"
)
func main() {
t := &testing.T{}
auditor, events, _ := audittest.NewQuick(t, "user_create", "user_delete")
_ = auditor.AuditEvent(audit.NewEvent("user_create", audit.Fields{
"any_field": "any_value",
}))
fmt.Println("count:", events.Count())
fmt.Println("type:", events.Events()[0].EventType)
}
Output: count: 1 type: user_create
func QuickTaxonomy ¶
QuickTaxonomy builds a minimal *audit.Taxonomy where every listed event type accepts any fields. All events are in a single enabled category ("test"). The returned taxonomy does not enforce required fields; pair it with audit.ValidationPermissive (as NewQuick does) for fully unconstrained testing.
func WriteKeyPEM ¶ added in v0.1.12
func WriteKeyPEM(tb testing.TB, path string, key *ecdsa.PrivateKey)
WriteKeyPEM writes an ECDSA private key as PEM to the given path.
Types ¶
type BadCerts ¶ added in v0.1.12
type BadCerts struct {
// CAPath is the self-signed CA that signed both bad-cert pairs.
CAPath string
// ExpiredCertPath / ExpiredKeyPath: NotAfter set in the past,
// SANs include localhost + 127.0.0.1.
ExpiredCertPath string
ExpiredKeyPath string
// CNMismatchCertPath / CNMismatchKeyPath: NotAfter valid but
// SANs only include "elsewhere.example.com" (CN/SAN does not
// match a client connecting to 127.0.0.1 or localhost).
CNMismatchCertPath string
CNMismatchKeyPath string
}
BadCerts holds a CA plus two server certificates with deliberate defects, used by negative-path TLS tests (#552). Both server certs are signed by the same runtime CA, so a client trusting CAPath fails for the installed defect — expired NotAfter or CN/SAN mismatch — rather than "unknown authority". A separate implementation in tests/bdd/steps/tls_negative_steps.go uses a different shape (in-memory tls.Config rather than file paths) and may be reconciled with this struct in a future PR.
func GenerateBadCerts ¶ added in v0.1.12
GenerateBadCerts produces a fresh CA and two server certificates (one expired, one with CN/SAN mismatch) under testing.TB.TempDir. ECDSA P-256. The two server cert pairs are independently usable in TLS server configs that need to be rejected by a client that trusts CAPath.
- Expired: NotAfter set one hour in the past; DNSNames "localhost" + IPAddresses 127.0.0.1.
- CN-mismatch: NotAfter valid; DNSNames "elsewhere.example.com".
Use ExpiredCert / CNMismatchCert for a single-cert convenience when only one defect is needed.
type MetricsRecorder ¶
type MetricsRecorder struct {
// contains filtered or unexported fields
}
MetricsRecorder implements audit.Metrics and captures all metric calls for assertion. It is safe for concurrent use.
MetricsRecorder also satisfies the output-specific extension interfaces ([file.RotationRecorder], [syslog.ReconnectRecorder]) via structural typing, enabling per-output rotation and reconnection recording.
func NewMetricsRecorder ¶
func NewMetricsRecorder() *MetricsRecorder
NewMetricsRecorder creates a MetricsRecorder ready for use with audit.WithMetrics.
func (*MetricsRecorder) BufferDrops ¶
func (m *MetricsRecorder) BufferDrops() int
BufferDrops returns the total number of buffer-full drops.
func (*MetricsRecorder) EventDeliveries ¶
func (m *MetricsRecorder) EventDeliveries(output string, status audit.EventStatus) int
EventDeliveries returns the number of delivery attempts recorded for the given output and audit.EventStatus (audit.EventSuccess or audit.EventError).
func (*MetricsRecorder) FileRotations ¶ added in v0.1.2
func (m *MetricsRecorder) FileRotations(path string) int
FileRotations returns the count of file rotations for the given path.
func (*MetricsRecorder) FilteredCount ¶
func (m *MetricsRecorder) FilteredCount(eventType string) int
FilteredCount returns the count of globally filtered events for the given event type.
func (*MetricsRecorder) OutputErrors ¶
func (m *MetricsRecorder) OutputErrors(output string) int
OutputErrors returns the count of write errors for the given output.
func (*MetricsRecorder) OutputFiltered ¶
func (m *MetricsRecorder) OutputFiltered(output string) int
OutputFiltered returns the count of per-output route-filtered events.
func (*MetricsRecorder) RecordBufferDrop ¶
func (m *MetricsRecorder) RecordBufferDrop()
RecordBufferDrop implements audit.Metrics.
func (*MetricsRecorder) RecordDelivery ¶ added in v0.1.12
func (m *MetricsRecorder) RecordDelivery(output string, status audit.EventStatus)
RecordDelivery implements audit.Metrics.
func (*MetricsRecorder) RecordFiltered ¶
func (m *MetricsRecorder) RecordFiltered(eventType string)
RecordFiltered implements audit.Metrics.
func (*MetricsRecorder) RecordOutputError ¶
func (m *MetricsRecorder) RecordOutputError(output string)
RecordOutputError implements audit.Metrics.
func (*MetricsRecorder) RecordOutputFiltered ¶
func (m *MetricsRecorder) RecordOutputFiltered(output string)
RecordOutputFiltered implements audit.Metrics.
func (*MetricsRecorder) RecordQueueDepth ¶ added in v0.1.10
func (m *MetricsRecorder) RecordQueueDepth(_, _ int)
RecordQueueDepth implements audit.Metrics.
func (*MetricsRecorder) RecordReconnect ¶ added in v0.1.12
func (m *MetricsRecorder) RecordReconnect(address string, success bool)
RecordReconnect satisfies syslog.ReconnectRecorder (#581).
func (*MetricsRecorder) RecordRotation ¶ added in v0.1.12
func (m *MetricsRecorder) RecordRotation(path string)
RecordRotation satisfies file.RotationRecorder (#581).
func (*MetricsRecorder) RecordSerializationError ¶
func (m *MetricsRecorder) RecordSerializationError(eventType string)
RecordSerializationError implements audit.Metrics.
func (*MetricsRecorder) RecordSubmitted ¶ added in v0.1.10
func (m *MetricsRecorder) RecordSubmitted()
RecordSubmitted implements audit.Metrics.
func (*MetricsRecorder) RecordValidationError ¶
func (m *MetricsRecorder) RecordValidationError(eventType string)
RecordValidationError implements audit.Metrics.
func (*MetricsRecorder) Reset ¶
func (m *MetricsRecorder) Reset()
Reset clears all recorded metrics.
func (*MetricsRecorder) SerializationErrors ¶
func (m *MetricsRecorder) SerializationErrors(eventType string) int
SerializationErrors returns the count of serialisation errors for the given event type.
func (*MetricsRecorder) SubmittedCount ¶ added in v0.1.10
func (m *MetricsRecorder) SubmittedCount() int
SubmittedCount returns the total number of events submitted via audit.Auditor.AuditEvent, before any filtering or buffering.
func (*MetricsRecorder) SyslogReconnects ¶ added in v0.1.2
func (m *MetricsRecorder) SyslogReconnects(address string, success bool) int
SyslogReconnects returns the count of syslog reconnections for the given address and outcome.
func (*MetricsRecorder) ValidationErrors ¶
func (m *MetricsRecorder) ValidationErrors(eventType string) int
ValidationErrors returns the count of validation errors recorded for the given event type.
type Option ¶
type Option func(*config)
Option configures the test auditor created by New.
func WithAsync ¶ added in v0.1.2
func WithAsync() Option
WithAsync creates an asynchronous test auditor. Events are delivered by a background goroutine, so callers MUST call auditor.Close() before making assertions. Use this only when testing async-specific behaviour such as drain timeout or buffer backpressure.
func WithAuditOption ¶ added in v0.1.10
WithAuditOption passes an arbitrary audit.Option through to the underlying audit.New call. Use for options not covered by the audittest.With* helpers (e.g., WithNamedOutput, WithFormatter, WithAppName, WithHost).
func WithDisabled ¶
func WithDisabled() Option
WithDisabled creates a disabled (no-op) test auditor. Events are accepted without error but not delivered.
func WithExcludeLabels ¶ added in v0.1.12
WithExcludeLabels applies sensitivity-label exclusion to the test Recorder, mirroring audit.WithExcludeLabels on a named output. Fields whose taxonomy labels match any of the given labels are stripped before delivery to the recorder.
Use this to unit-test compliance workflows — e.g., verify that a "pii"-labelled field does not appear in an output configured for an external analytics pipeline. The taxonomy passed to New MUST define a sensitivity section covering every label, else audit.New returns an error and the test fails at construction.
outputName MUST match the recorder's name — "recorder" by default, or whatever was passed to NewNamedRecorder. The parameter is explicit for symmetry with audit.WithNamedOutput and to leave room for future multi-output audittest expansion. A mismatch causes New / NewQuick to call tb.Fatalf.
Multiple calls accumulate: WithExcludeLabels("recorder", "pii"), WithExcludeLabels("recorder", "financial") strips both. Passing no labels (WithExcludeLabels("recorder")) is a no-op at the strip level — it still engages the named-output plumbing branch but no fields are stripped. Framework fields are never stripped, per audit.WithExcludeLabels.
Example ¶
ExampleWithExcludeLabels shows how to assert that a compliance output does NOT receive pii-labelled fields. The recorder acts as the compliance destination; labels defined in the taxonomy drive the strip.
package main
import (
"fmt"
"testing"
"github.com/axonops/audit"
"github.com/axonops/audit/audittest"
)
// exampleSensitivityTaxonomyYAML registers a "pii" sensitivity label
// so ExampleWithExcludeLabels can exercise the strip path.
var exampleSensitivityTaxonomyYAML = []byte(`
version: 1
sensitivity:
labels:
pii:
fields: [email]
categories:
write:
- user_create
events:
user_create:
fields:
outcome: {required: true}
actor_id: {required: true}
email: {}
`)
func main() {
t := &testing.T{}
auditor, events, _ := audittest.New(t, exampleSensitivityTaxonomyYAML,
audittest.WithExcludeLabels("recorder", "pii"),
)
_ = auditor.AuditEvent(audit.NewEvent("user_create", audit.Fields{
"outcome": "success",
"actor_id": "alice",
"email": "alice@example.com",
}))
evt := events.Events()[0]
fmt.Println("actor_id:", evt.StringField("actor_id"))
fmt.Println("email present:", evt.Field("email") != nil)
}
Output: actor_id: alice email present: false
func WithSync ¶
func WithSync() Option
WithSync creates a synchronous test auditor where events are available in the Recorder immediately after audit.Auditor.AuditEvent returns. No Close-before-assert ceremony is needed.
Both New and NewQuick default to synchronous delivery, so this option is only needed to re-enable sync after WithAsync.
func WithValidationMode ¶
func WithValidationMode(mode audit.ValidationMode) Option
WithValidationMode sets the taxonomy validation mode. Default is audit.ValidationStrict.
func WithVerbose ¶ added in v0.1.10
func WithVerbose() Option
WithVerbose re-enables diagnostic log output from the auditor. By default, test auditors silence diagnostic logs (lifecycle messages, shutdown notices) to keep test output clean. Use this option when debugging auditor behaviour in tests.
type OutputMetricsRecorder ¶ added in v0.1.10
type OutputMetricsRecorder struct {
// contains filtered or unexported fields
}
OutputMetricsRecorder implements audit.OutputMetrics and captures all per-output metric calls for assertion. Pass it to an output's `WithOutputMetrics` constructor option when testing custom outputs. It is safe for concurrent use.
func NewOutputMetricsRecorder ¶ added in v0.1.10
func NewOutputMetricsRecorder() *OutputMetricsRecorder
NewOutputMetricsRecorder creates an OutputMetricsRecorder.
func (*OutputMetricsRecorder) DropCount ¶ added in v0.1.10
func (m *OutputMetricsRecorder) DropCount() int
DropCount returns the number of recorded drops.
func (*OutputMetricsRecorder) ErrorCount ¶ added in v0.1.10
func (m *OutputMetricsRecorder) ErrorCount() int
ErrorCount returns the number of recorded errors.
func (*OutputMetricsRecorder) FlushCount ¶ added in v0.1.10
func (m *OutputMetricsRecorder) FlushCount() int
FlushCount returns the number of recorded flushes.
func (*OutputMetricsRecorder) RecordDrop ¶ added in v0.1.10
func (m *OutputMetricsRecorder) RecordDrop()
RecordDrop implements audit.OutputMetrics.
func (*OutputMetricsRecorder) RecordError ¶ added in v0.1.10
func (m *OutputMetricsRecorder) RecordError()
RecordError implements audit.OutputMetrics.
func (*OutputMetricsRecorder) RecordFlush ¶ added in v0.1.10
func (m *OutputMetricsRecorder) RecordFlush(_ int, d time.Duration)
RecordFlush implements audit.OutputMetrics.
func (*OutputMetricsRecorder) RecordQueueDepth ¶ added in v0.1.10
func (m *OutputMetricsRecorder) RecordQueueDepth(_, _ int)
RecordQueueDepth implements audit.OutputMetrics.
func (*OutputMetricsRecorder) RecordRetry ¶ added in v0.1.10
func (m *OutputMetricsRecorder) RecordRetry(_ int)
RecordRetry implements audit.OutputMetrics.
func (*OutputMetricsRecorder) Reset ¶ added in v0.1.10
func (m *OutputMetricsRecorder) Reset()
Reset clears all recorded output metrics.
func (*OutputMetricsRecorder) RetryCount ¶ added in v0.1.10
func (m *OutputMetricsRecorder) RetryCount() int
RetryCount returns the number of recorded retries.
type RecordedEvent ¶
type RecordedEvent struct {
// EventType is the event type name (e.g., "user_create").
EventType string
// Severity is the resolved severity (0-10).
Severity int
// Timestamp is the event timestamp set by the drain goroutine.
Timestamp time.Time
// Fields contains all non-framework fields. Note: numeric values
// are stored as float64 due to JSON round-tripping. Use
// [RecordedEvent.IntField] for int assertions.
Fields map[string]any
// RawJSON is the original serialised bytes for format-level assertions.
RawJSON []byte
// ParseErr holds any error from JSON deserialisation of the raw bytes.
// When non-nil, EventType, Severity, Timestamp, and Fields are all
// zero-valued. Tests SHOULD assert ParseErr == nil before inspecting
// other fields to avoid masking serialisation bugs.
ParseErr error
}
RecordedEvent is a single captured audit event with structured access to its fields. Events are captured after serialisation and deserialised back into structured form.
Fields contains the event's non-framework field values. Values follow encoding/json conventions: numbers are float64, nested objects are map[string]interface{}, arrays are []interface{}.
func (RecordedEvent) BoolField ¶ added in v0.1.10
func (e RecordedEvent) BoolField(key string) bool
BoolField returns the value of the named field as a bool. Returns false if the key is missing or the value is not a bool.
func (RecordedEvent) Field ¶
func (e RecordedEvent) Field(key string) any
Field returns the value of the named field, or nil if not present.
func (RecordedEvent) FloatField ¶
func (e RecordedEvent) FloatField(key string) float64
FloatField returns the value of the named field as a float64. Returns 0 if the key is missing or the value is not numeric.
func (RecordedEvent) GoString ¶
func (e RecordedEvent) GoString() string
GoString returns a human-readable representation of the event, suitable for test failure messages.
func (RecordedEvent) HasField ¶
func (e RecordedEvent) HasField(key string, want any) bool
HasField reports whether the event has a field with the given key and value. Comparison uses reflect.DeepEqual.
func (RecordedEvent) IntField ¶
func (e RecordedEvent) IntField(key string) int
IntField returns the value of the named field as an int. JSON round-tripping stores all numbers as float64, so this method handles the float64→int coercion transparently.
func (RecordedEvent) StringField ¶
func (e RecordedEvent) StringField(key string) string
StringField returns the value of the named field as a string. Returns the empty string if the key is missing or the value is not a string.
func (RecordedEvent) UserFields ¶ added in v0.1.2
func (e RecordedEvent) UserFields() map[string]any
UserFields returns a copy of the event's fields with framework fields removed (event_category, app_name, host, timezone, pid, duration_ms, _hmac, _hmac_version). This is useful for count assertions where framework fields would inflate the total.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder implements audit.Output and captures events in memory for assertion. It is safe for concurrent use by the drain goroutine (writing) and the test goroutine (reading).
The Recorder only parses JSON-formatted events. Events formatted with CEFFormatter will have ParseErr set and structured fields will be zero-valued.
func NewNamedRecorder ¶ added in v0.1.10
NewNamedRecorder creates a Recorder with a custom name. The name is returned by Recorder.Name and appears in metrics keys.
func NewRecorder ¶
func NewRecorder() *Recorder
NewRecorder creates a Recorder with the default name "recorder". Use it with audit.WithOutputs or audit.WithNamedOutput when composing an auditor manually. For the common case, use New or NewQuick instead.
func (*Recorder) AssertContains ¶ added in v0.1.10
AssertContains asserts that at least one event of the given type was recorded with fields matching every key-value pair in fields. It calls tb.Error (not Fatal) on failure, allowing further assertions to run.
func (*Recorder) Close ¶
Close implements audit.Output. It is a no-op — the Recorder does not own any resources.
func (*Recorder) Events ¶
func (r *Recorder) Events() []RecordedEvent
Events returns a snapshot of all recorded events in drain order. The returned slice is a shallow copy of the event list. Later Reset calls do not affect the slice, but the Fields maps within events share references with the Recorder's internal state. Call after audit.Auditor.Close to ensure all events have been processed.
func (*Recorder) FindByField ¶ added in v0.1.10
func (r *Recorder) FindByField(key string, val any) []RecordedEvent
FindByField returns all recorded events where the given field matches val (compared with reflect.DeepEqual).
func (*Recorder) FindByType ¶
func (r *Recorder) FindByType(eventType string) []RecordedEvent
FindByType returns all recorded events matching the given event type.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/axonops/audit"
"github.com/axonops/audit/audittest"
)
func main() {
t := &testing.T{}
auditor, events, _ := audittest.NewQuick(t, "user_create", "auth_failure")
_ = auditor.AuditEvent(audit.NewEvent("user_create", audit.Fields{"actor_id": "alice"}))
_ = auditor.AuditEvent(audit.NewEvent("auth_failure", audit.Fields{"actor_id": "bob"}))
_ = auditor.AuditEvent(audit.NewEvent("user_create", audit.Fields{"actor_id": "charlie"}))
creates := events.FindByType("user_create")
fmt.Println("user_create count:", len(creates))
fmt.Println("auth_failure count:", len(events.FindByType("auth_failure")))
}
Output: user_create count: 2 auth_failure count: 1
func (*Recorder) First ¶ added in v0.1.10
func (r *Recorder) First() (RecordedEvent, bool)
First returns the earliest recorded event, or false if the Recorder is empty.
func (*Recorder) GoString ¶
GoString returns a human-readable summary of all recorded events, suitable for test failure messages.
func (*Recorder) Last ¶ added in v0.1.10
func (r *Recorder) Last() (RecordedEvent, bool)
Last returns the most recently recorded event, or false if the Recorder is empty.
func (*Recorder) RequireEmpty ¶ added in v0.1.10
RequireEmpty asserts that no events have been recorded. It calls tb.Fatal if any events are present.
func (*Recorder) RequireEvent ¶ added in v0.1.10
func (r *Recorder) RequireEvent(tb testing.TB, eventType string) RecordedEvent
RequireEvent asserts that exactly one event of the given type was recorded and returns it. It calls tb.Fatal if the count is not 1.
func (*Recorder) RequireEvents ¶ added in v0.1.10
func (r *Recorder) RequireEvents(tb testing.TB, n int) []RecordedEvent
RequireEvents asserts that exactly n events were recorded and returns them. It calls tb.Fatal if the count does not match.
func (*Recorder) Reset ¶
func (r *Recorder) Reset()
Reset clears all recorded events. The underlying auditor remains open and functional. Use Reset between sub-tests to isolate assertions without creating a new auditor.
func (*Recorder) WaitForN ¶ added in v0.1.12
WaitForN blocks until at least n events have been recorded or timeout elapses. It returns true if the target was reached, false on timeout.
Use WaitForN in tests built with WithAsync: events are delivered by a background goroutine, so assertions made immediately after audit.Auditor.AuditEvent may race the drain. WaitForN provides a deterministic wait-until-ready barrier without time.Sleep.
For synchronous auditors (the default for New and NewQuick), events are available immediately after AuditEvent returns — prefer Recorder.Count / Recorder.RequireEvents there. If the target is already reached at call time, WaitForN returns true without sleeping.
The poll interval is 10 ms and is not configurable — pass a larger timeout to tolerate slower CI environments. Pass tb so that subsequent caller-side assertions on the returned bool attribute failure lines to the calling test.
Example ¶
ExampleRecorder_WaitForN shows how to wait for asynchronously- delivered events to land in the Recorder before asserting. Use WaitForN with WithAsync or any auditor configured for async delivery — synchronous auditors do not need it.
package main
import (
"fmt"
"testing"
"time"
"github.com/axonops/audit"
"github.com/axonops/audit/audittest"
)
var exampleTaxonomyYAML = []byte(`
version: 1
categories:
write:
- user_create
events:
user_create:
fields:
outcome: {required: true}
actor_id: {required: true}
`)
func main() {
t := &testing.T{}
auditor, events, _ := audittest.New(t, exampleTaxonomyYAML, audittest.WithAsync())
defer func() { _ = auditor.Close() }() // stub *testing.T has no Cleanup — close explicitly
// Emit events from a goroutine — simulates a service emitting
// audit events on a hot path.
go func() {
for i := 0; i < 3; i++ {
_ = auditor.AuditEvent(audit.NewEvent("user_create", audit.Fields{
"outcome": "success",
"actor_id": "alice",
}))
}
}()
if ok := events.WaitForN(t, 3, 2*time.Second); !ok {
fmt.Println("timeout waiting for 3 events")
return
}
fmt.Println("count:", events.Count())
}
Output: count: 3
type TestCerts ¶ added in v0.1.12
type TestCerts struct {
// TLSCfg is a server-side [tls.Config] with the server certificate
// loaded, ClientCAs set to a pool containing the CA cert, and
// ClientAuth set to [tls.VerifyClientCertIfGiven].
TLSCfg *tls.Config
// CAPath is the PEM-encoded self-signed CA certificate.
CAPath string
// CertPath is the PEM-encoded server certificate (CN=localhost,
// SANs include localhost + 127.0.0.1).
CertPath string
// KeyPath is the PEM-encoded server private key (ECDSA P-256).
KeyPath string
// ClientCert is the PEM-encoded client certificate
// (CN=test-client, ExtKeyUsageClientAuth).
ClientCert string
// ClientKey is the PEM-encoded client private key (ECDSA P-256).
ClientKey string
}
TestCerts holds paths to test TLS certificates and a server TLS config produced by GenerateTestCerts. All files are written under testing.TB.TempDir so they are cleaned up automatically when the test exits.
func GenerateTestCerts ¶ added in v0.1.12
GenerateTestCerts creates a self-signed CA plus server and client certificates for testing TLS. All files are written to testing.TB.TempDir and removed when the test exits. ECDSA P-256 keys, one-hour expiry. The returned tls.Config is ready to use with net.Listen's tls wrapper for a server that wants to verify optional client certs against the same CA.