feat: implement T05, T08, T13 — OIDC discovery, JWKS, telemetry pipeline
- T05: /.well-known/openid-configuration — profile-only features advertised - T08: /jwks — RS256 JWK Set, stdlib crypto only, key rotation support - T13: Structured telemetry — Event types, LogEmitter/NoopEmitter/MultiEmitter, context helpers 38 server tests pass, go vet clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
329e996619
commit
22f7a7dc50
9 changed files with 1080 additions and 1 deletions
179
src/internal/server/telemetry/emitter_test.go
Normal file
179
src/internal/server/telemetry/emitter_test.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue