180 lines
4.4 KiB
Go
180 lines
4.4 KiB
Go
|
|
package telemetry_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/rs/zerolog"
|
||
|
|
|
||
|
|
"keycape/internal/server/telemetry"
|
||
|
|
)
|
||
|
|
|
||
|
|
func sampleEvent() telemetry.Event {
|
||
|
|
return telemetry.Event{
|
||
|
|
Timestamp: time.Now().UTC(),
|
||
|
|
ClientID: "test-client",
|
||
|
|
Endpoint: "/oauth2/token",
|
||
|
|
Feature: "",
|
||
|
|
Result: "success",
|
||
|
|
ErrorType: "",
|
||
|
|
Scopes: []string{"openid", "profile"},
|
||
|
|
GrantType: "authorization_code",
|
||
|
|
Environment: "test",
|
||
|
|
TraceID: "trace-abc-123",
|
||
|
|
EventType: telemetry.EventTokenIssued,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- NoopEmitter ----
|
||
|
|
|
||
|
|
func TestNoopEmitter_DoesNotPanic(t *testing.T) {
|
||
|
|
e := telemetry.NoopEmitter{}
|
||
|
|
e.Emit(context.Background(), sampleEvent())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNoopEmitter_ImplementsInterface(t *testing.T) {
|
||
|
|
var _ telemetry.Emitter = telemetry.NoopEmitter{}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- LogEmitter ----
|
||
|
|
|
||
|
|
func TestLogEmitter_WritesJSON(t *testing.T) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
logger := zerolog.New(&buf)
|
||
|
|
e := telemetry.NewLogEmitter(logger)
|
||
|
|
|
||
|
|
ev := sampleEvent()
|
||
|
|
e.Emit(context.Background(), ev)
|
||
|
|
|
||
|
|
if buf.Len() == 0 {
|
||
|
|
t.Fatal("expected output from LogEmitter, got nothing")
|
||
|
|
}
|
||
|
|
|
||
|
|
var out map[string]interface{}
|
||
|
|
if err := json.Unmarshal(buf.Bytes(), &out); err != nil {
|
||
|
|
t.Fatalf("LogEmitter output is not valid JSON: %v\noutput: %s", err, buf.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLogEmitter_ContainsEventFields(t *testing.T) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
logger := zerolog.New(&buf)
|
||
|
|
e := telemetry.NewLogEmitter(logger)
|
||
|
|
|
||
|
|
ev := sampleEvent()
|
||
|
|
e.Emit(context.Background(), ev)
|
||
|
|
|
||
|
|
var out map[string]interface{}
|
||
|
|
_ = json.Unmarshal(buf.Bytes(), &out)
|
||
|
|
|
||
|
|
requiredFields := []string{"client_id", "endpoint", "result", "environment", "trace_id", "event_type"}
|
||
|
|
for _, f := range requiredFields {
|
||
|
|
if _, ok := out[f]; !ok {
|
||
|
|
t.Errorf("LogEmitter output missing field %q", f)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLogEmitter_EventTypeValue(t *testing.T) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
logger := zerolog.New(&buf)
|
||
|
|
e := telemetry.NewLogEmitter(logger)
|
||
|
|
|
||
|
|
ev := sampleEvent()
|
||
|
|
ev.EventType = telemetry.EventAuthFailure
|
||
|
|
e.Emit(context.Background(), ev)
|
||
|
|
|
||
|
|
var out map[string]interface{}
|
||
|
|
_ = json.Unmarshal(buf.Bytes(), &out)
|
||
|
|
|
||
|
|
if out["event_type"] != string(telemetry.EventAuthFailure) {
|
||
|
|
t.Errorf("event_type: expected %q, got %v", telemetry.EventAuthFailure, out["event_type"])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLogEmitter_ImplementsInterface(t *testing.T) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
logger := zerolog.New(&buf)
|
||
|
|
var _ telemetry.Emitter = telemetry.NewLogEmitter(logger)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- MultiEmitter ----
|
||
|
|
|
||
|
|
type capturingEmitter struct {
|
||
|
|
events []telemetry.Event
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *capturingEmitter) Emit(_ context.Context, ev telemetry.Event) {
|
||
|
|
c.events = append(c.events, ev)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMultiEmitter_FansOut(t *testing.T) {
|
||
|
|
a := &capturingEmitter{}
|
||
|
|
b := &capturingEmitter{}
|
||
|
|
m := telemetry.NewMultiEmitter(a, b)
|
||
|
|
|
||
|
|
ev := sampleEvent()
|
||
|
|
m.Emit(context.Background(), ev)
|
||
|
|
|
||
|
|
if len(a.events) != 1 {
|
||
|
|
t.Errorf("emitter a: expected 1 event, got %d", len(a.events))
|
||
|
|
}
|
||
|
|
if len(b.events) != 1 {
|
||
|
|
t.Errorf("emitter b: expected 1 event, got %d", len(b.events))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMultiEmitter_EmptyIsNoop(t *testing.T) {
|
||
|
|
m := telemetry.NewMultiEmitter()
|
||
|
|
m.Emit(context.Background(), sampleEvent()) // must not panic
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMultiEmitter_ImplementsInterface(t *testing.T) {
|
||
|
|
var _ telemetry.Emitter = telemetry.NewMultiEmitter()
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- Context helpers ----
|
||
|
|
|
||
|
|
func TestWithEmitter_RoundTrip(t *testing.T) {
|
||
|
|
orig := telemetry.NoopEmitter{}
|
||
|
|
ctx := telemetry.WithEmitter(context.Background(), orig)
|
||
|
|
got := telemetry.EmitterFromContext(ctx)
|
||
|
|
if got == nil {
|
||
|
|
t.Fatal("EmitterFromContext returned nil after WithEmitter")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEmitterFromContext_FallsBackToNoop(t *testing.T) {
|
||
|
|
got := telemetry.EmitterFromContext(context.Background())
|
||
|
|
if got == nil {
|
||
|
|
t.Fatal("EmitterFromContext must never return nil — fallback to NoopEmitter expected")
|
||
|
|
}
|
||
|
|
// Verify the fallback doesn't panic
|
||
|
|
got.Emit(context.Background(), sampleEvent())
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- EventType constants ----
|
||
|
|
|
||
|
|
func TestEventTypeConstants(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
et telemetry.EventType
|
||
|
|
want string
|
||
|
|
}{
|
||
|
|
{telemetry.EventAuthStart, "auth_start"},
|
||
|
|
{telemetry.EventAuthSuccess, "auth_success"},
|
||
|
|
{telemetry.EventAuthFailure, "auth_failure"},
|
||
|
|
{telemetry.EventTokenIssued, "token_issued"},
|
||
|
|
{telemetry.EventUnsupportedFeature, "unsupported_feature"},
|
||
|
|
{telemetry.EventInvalidRequest, "invalid_request"},
|
||
|
|
{telemetry.EventMigration, "migration_event"},
|
||
|
|
}
|
||
|
|
for _, c := range cases {
|
||
|
|
if string(c.et) != c.want {
|
||
|
|
t.Errorf("EventType %q: expected %q", c.et, c.want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|