Skip to content

Commit 8775c87

Browse files
test: improved tests coverage for audit logs
1 parent bc2613c commit 8775c87

3 files changed

Lines changed: 250 additions & 0 deletions

File tree

config/config_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,64 @@ OPENAI_API_KEY=sk-from-dotenv-file
267267
}
268268
}
269269

270+
func TestLoggingOnlyModelInteractionsDefault(t *testing.T) {
271+
// Reset viper state before test
272+
viper.Reset()
273+
274+
// Clear all relevant environment variables
275+
_ = os.Unsetenv("LOGGING_ONLY_MODEL_INTERACTIONS")
276+
_ = os.Unsetenv("OPENAI_API_KEY")
277+
278+
cfg, err := Load()
279+
if err != nil {
280+
t.Fatalf("Load() failed: %v", err)
281+
}
282+
283+
// Default should be true
284+
if !cfg.Logging.OnlyModelInteractions {
285+
t.Error("expected OnlyModelInteractions to default to true")
286+
}
287+
}
288+
289+
func TestLoggingOnlyModelInteractionsFromEnv(t *testing.T) {
290+
tests := []struct {
291+
name string
292+
envValue string
293+
expected bool
294+
}{
295+
{"true lowercase", "true", true},
296+
{"TRUE uppercase", "TRUE", true},
297+
{"True mixed", "True", true},
298+
{"false lowercase", "false", false},
299+
{"FALSE uppercase", "FALSE", false},
300+
{"False mixed", "False", false},
301+
{"1 numeric", "1", true},
302+
{"0 numeric", "0", false},
303+
}
304+
305+
for _, tt := range tests {
306+
t.Run(tt.name, func(t *testing.T) {
307+
// Reset viper state before each subtest
308+
viper.Reset()
309+
310+
// Clear and set environment variable
311+
_ = os.Unsetenv("OPENAI_API_KEY")
312+
_ = os.Setenv("LOGGING_ONLY_MODEL_INTERACTIONS", tt.envValue)
313+
defer func() { _ = os.Unsetenv("LOGGING_ONLY_MODEL_INTERACTIONS") }()
314+
315+
cfg, err := Load()
316+
if err != nil {
317+
t.Fatalf("Load() failed: %v", err)
318+
}
319+
320+
if cfg.Logging.OnlyModelInteractions != tt.expected {
321+
t.Errorf("expected OnlyModelInteractions=%v for env value %q, got %v",
322+
tt.expected, tt.envValue, cfg.Logging.OnlyModelInteractions)
323+
}
324+
})
325+
}
326+
}
327+
270328
func TestValidateBodySizeLimit(t *testing.T) {
271329
tests := []struct {
272330
name string

internal/auditlog/auditlog_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,37 @@ func TestSkipLoggingPaths(t *testing.T) {
349349
}
350350
}
351351

352+
func TestIsModelInteractionPath(t *testing.T) {
353+
tests := []struct {
354+
name string
355+
path string
356+
expected bool
357+
}{
358+
{"chat completions", "/v1/chat/completions", true},
359+
{"chat completions with query", "/v1/chat/completions?stream=true", true},
360+
{"responses", "/v1/responses", true},
361+
{"responses with subpath", "/v1/responses/123", true},
362+
{"models", "/v1/models", true},
363+
{"models with subpath", "/v1/models/gpt-4", true},
364+
{"health", "/health", false},
365+
{"metrics", "/metrics", false},
366+
{"admin", "/admin", false},
367+
{"root", "/", false},
368+
{"empty", "", false},
369+
{"v1 prefix only", "/v1", false},
370+
{"v1 other endpoint", "/v1/other", false},
371+
}
372+
373+
for _, tt := range tests {
374+
t.Run(tt.name, func(t *testing.T) {
375+
result := IsModelInteractionPath(tt.path)
376+
if result != tt.expected {
377+
t.Errorf("IsModelInteractionPath(%q) = %v, want %v", tt.path, result, tt.expected)
378+
}
379+
})
380+
}
381+
}
382+
352383
func TestParseUsageFromSSE(t *testing.T) {
353384
tests := []struct {
354385
name string

tests/e2e/auditlog_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,164 @@ func TestAuditLogErrorCapture(t *testing.T) {
607607
assert.Equal(t, http.StatusBadRequest, entry.StatusCode)
608608
})
609609
}
610+
611+
func TestAuditLogOnlyModelInteractions(t *testing.T) {
612+
t.Run("logs model endpoints when OnlyModelInteractions enabled", func(t *testing.T) {
613+
store := newMockLogStore()
614+
cfg := auditlog.Config{
615+
Enabled: true,
616+
LogBodies: false,
617+
LogHeaders: false,
618+
BufferSize: 100,
619+
FlushInterval: 100 * time.Millisecond,
620+
OnlyModelInteractions: true,
621+
}
622+
623+
serverURL, srv, logger := setupAuditLogTestServer(t, cfg, store)
624+
defer func() {
625+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
626+
defer cancel()
627+
_ = srv.Shutdown(ctx)
628+
_ = logger.Close()
629+
}()
630+
631+
// Make a request to a model endpoint
632+
payload := core.ChatRequest{
633+
Model: "gpt-4",
634+
Messages: []core.Message{{Role: "user", Content: "Hello"}},
635+
}
636+
body, _ := json.Marshal(payload)
637+
resp, err := http.Post(serverURL+"/v1/chat/completions", "application/json", bytes.NewReader(body))
638+
require.NoError(t, err)
639+
defer closeBody(resp)
640+
require.Equal(t, http.StatusOK, resp.StatusCode)
641+
642+
// Wait for log entry to be written
643+
entries := store.WaitForEntries(1, 2*time.Second)
644+
require.Len(t, entries, 1, "Expected 1 log entry for model endpoint")
645+
646+
entry := entries[0]
647+
assert.Equal(t, "/v1/chat/completions", entry.Data.Path)
648+
})
649+
650+
t.Run("skips health endpoint when OnlyModelInteractions enabled", func(t *testing.T) {
651+
store := newMockLogStore()
652+
cfg := auditlog.Config{
653+
Enabled: true,
654+
LogBodies: false,
655+
LogHeaders: false,
656+
BufferSize: 100,
657+
FlushInterval: 100 * time.Millisecond,
658+
OnlyModelInteractions: true,
659+
}
660+
661+
serverURL, srv, logger := setupAuditLogTestServer(t, cfg, store)
662+
defer func() {
663+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
664+
defer cancel()
665+
_ = srv.Shutdown(ctx)
666+
_ = logger.Close()
667+
}()
668+
669+
// Make multiple requests to health endpoint
670+
for i := 0; i < 3; i++ {
671+
resp, err := http.Get(serverURL + "/health")
672+
require.NoError(t, err)
673+
closeBody(resp)
674+
}
675+
676+
// Wait a bit and check that NO entries were logged
677+
time.Sleep(500 * time.Millisecond)
678+
entries := store.GetEntries()
679+
assert.Empty(t, entries, "Expected no log entries for health endpoint when OnlyModelInteractions=true")
680+
})
681+
682+
t.Run("logs health endpoint when OnlyModelInteractions disabled", func(t *testing.T) {
683+
store := newMockLogStore()
684+
cfg := auditlog.Config{
685+
Enabled: true,
686+
LogBodies: false,
687+
LogHeaders: false,
688+
BufferSize: 100,
689+
FlushInterval: 100 * time.Millisecond,
690+
OnlyModelInteractions: false,
691+
}
692+
693+
serverURL, srv, logger := setupAuditLogTestServer(t, cfg, store)
694+
defer func() {
695+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
696+
defer cancel()
697+
_ = srv.Shutdown(ctx)
698+
_ = logger.Close()
699+
}()
700+
701+
// Note: setupAuditLogTestServer makes health check calls during startup
702+
// so we may already have entries. Count before making our request.
703+
time.Sleep(200 * time.Millisecond) // Wait for any buffered entries to flush
704+
entriesBefore := len(store.GetEntries())
705+
706+
// Make requests to health endpoint
707+
resp, err := http.Get(serverURL + "/health")
708+
require.NoError(t, err)
709+
closeBody(resp)
710+
711+
// Wait for log entry to be written (at least 1 more than before)
712+
entries := store.WaitForEntries(entriesBefore+1, 2*time.Second)
713+
require.Greater(t, len(entries), entriesBefore, "Expected new log entry for health endpoint when OnlyModelInteractions=false")
714+
715+
// The last entry should be our health request
716+
lastEntry := entries[len(entries)-1]
717+
assert.Equal(t, "/health", lastEntry.Data.Path)
718+
})
719+
720+
t.Run("filters mixed requests correctly", func(t *testing.T) {
721+
store := newMockLogStore()
722+
cfg := auditlog.Config{
723+
Enabled: true,
724+
LogBodies: false,
725+
LogHeaders: false,
726+
BufferSize: 100,
727+
FlushInterval: 100 * time.Millisecond,
728+
OnlyModelInteractions: true,
729+
}
730+
731+
serverURL, srv, logger := setupAuditLogTestServer(t, cfg, store)
732+
defer func() {
733+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
734+
defer cancel()
735+
_ = srv.Shutdown(ctx)
736+
_ = logger.Close()
737+
}()
738+
739+
// Make multiple health requests (should not be logged)
740+
for i := 0; i < 5; i++ {
741+
resp, err := http.Get(serverURL + "/health")
742+
require.NoError(t, err)
743+
closeBody(resp)
744+
}
745+
746+
// Make a model request (should be logged)
747+
payload := core.ChatRequest{
748+
Model: "gpt-4",
749+
Messages: []core.Message{{Role: "user", Content: "Hello"}},
750+
}
751+
body, _ := json.Marshal(payload)
752+
resp, err := http.Post(serverURL+"/v1/chat/completions", "application/json", bytes.NewReader(body))
753+
require.NoError(t, err)
754+
closeBody(resp)
755+
756+
// Make more health requests (should not be logged)
757+
for i := 0; i < 3; i++ {
758+
resp, err := http.Get(serverURL + "/health")
759+
require.NoError(t, err)
760+
closeBody(resp)
761+
}
762+
763+
// Wait for log entry to be written
764+
entries := store.WaitForEntries(1, 2*time.Second)
765+
766+
// Should only have the model endpoint logged
767+
require.Len(t, entries, 1, "Expected only 1 log entry (model endpoint)")
768+
assert.Equal(t, "/v1/chat/completions", entries[0].Data.Path)
769+
})
770+
}

0 commit comments

Comments
 (0)