package monitoring import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestLogFilters_GetLimit(t *testing.T) { tests := []struct { name string limit int expected int }{ {"default for zero", 0, 100}, {"default for negative", -5, 100}, {"capped at 1000", 2000, 1000}, {"exactly 1000", 1000, 1000}, {"normal value", 50, 50}, {"minimum valid", 1, 1}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { f := LogFilters{Limit: tc.limit} assert.Equal(t, tc.expected, f.GetLimit()) }) } } func TestHTTPStatsCollector_Record_And_GetStats(t *testing.T) { c := NewHTTPStatsCollector() c.Record("GET /api/tasks/", 100*time.Millisecond, 200) c.Record("GET /api/tasks/", 200*time.Millisecond, 200) c.Record("POST /api/tasks/", 50*time.Millisecond, 201) c.Record("GET /api/tasks/", 300*time.Millisecond, 500) stats := c.GetStats() assert.Equal(t, int64(4), stats.RequestsTotal) assert.True(t, stats.RequestsPerMinute > 0) // Check endpoint stats taskStats, ok := stats.ByEndpoint["GET /api/tasks/"] assert.True(t, ok) assert.Equal(t, int64(3), taskStats.Count) assert.True(t, taskStats.AvgLatencyMs > 0) postStats, ok := stats.ByEndpoint["POST /api/tasks/"] assert.True(t, ok) assert.Equal(t, int64(1), postStats.Count) // Check status codes assert.Equal(t, int64(2), stats.ByStatusCode[200]) assert.Equal(t, int64(1), stats.ByStatusCode[201]) assert.Equal(t, int64(1), stats.ByStatusCode[500]) // Error rate should include 500 status assert.True(t, stats.ErrorRate > 0) } func TestHTTPStatsCollector_Reset(t *testing.T) { c := NewHTTPStatsCollector() c.Record("GET /api/tasks/", 100*time.Millisecond, 200) c.Reset() stats := c.GetStats() assert.Equal(t, int64(0), stats.RequestsTotal) assert.Empty(t, stats.ByEndpoint) assert.Empty(t, stats.ByStatusCode) } func TestHTTPStatsCollector_ErrorRate(t *testing.T) { c := NewHTTPStatsCollector() c.Record("GET /api/tasks/", 10*time.Millisecond, 200) c.Record("GET /api/tasks/", 10*time.Millisecond, 400) c.Record("GET /api/tasks/", 10*time.Millisecond, 500) stats := c.GetStats() ep := stats.ByEndpoint["GET /api/tasks/"] // 2 out of 3 are errors (400 and 500) assert.InDelta(t, 2.0/3.0, ep.ErrorRate, 0.001) } func TestHTTPStatsCollector_P95(t *testing.T) { c := NewHTTPStatsCollector() // Record 100 requests with increasing latencies for i := 1; i <= 100; i++ { c.Record("GET /api/test/", time.Duration(i)*time.Millisecond, 200) } stats := c.GetStats() ep := stats.ByEndpoint["GET /api/test/"] // P95 should be around 95ms assert.True(t, ep.P95LatencyMs >= 90, "P95 should be >= 90ms, got %f", ep.P95LatencyMs) } func TestHTTPStatsCollector_EmptyStats(t *testing.T) { c := NewHTTPStatsCollector() stats := c.GetStats() assert.Equal(t, int64(0), stats.RequestsTotal) assert.Equal(t, float64(0), stats.AvgLatencyMs) assert.Equal(t, float64(0), stats.ErrorRate) assert.Equal(t, float64(0), stats.RequestsPerMinute) } func TestHTTPStatsCollector_EndpointOverflow(t *testing.T) { c := NewHTTPStatsCollector() // Fill up to maxEndpoints unique endpoints for i := 0; i < maxEndpoints+10; i++ { endpoint := "GET /api/test/" + string(rune('A'+i%26)) + string(rune('0'+i/26)) c.Record(endpoint, 10*time.Millisecond, 200) } stats := c.GetStats() // Should have at most maxEndpoints + 1 (the OTHER bucket) assert.LessOrEqual(t, len(stats.ByEndpoint), maxEndpoints+1) } func TestWSMessageConstants(t *testing.T) { assert.Equal(t, "log", WSMessageTypeLog) assert.Equal(t, "stats", WSMessageTypeStats) } func TestRedisLogWriter_Write_Disabled(t *testing.T) { // Create a writer with a nil buffer -- won't actually push to Redis // but we can test the enabled/disabled logic w := &RedisLogWriter{ process: "api", ch: make(chan LogEntry, writerChannelSize), done: make(chan struct{}), } w.enabled.Store(false) // Start drain loop (reads from channel) go func() { defer close(w.done) for range w.ch { } }() n, err := w.Write([]byte(`{"level":"info","message":"test"}`)) assert.NoError(t, err) assert.Greater(t, n, 0) // Channel should be empty since writer is disabled assert.Equal(t, 0, len(w.ch)) close(w.ch) <-w.done } func TestRedisLogWriter_Write_Enabled(t *testing.T) { w := &RedisLogWriter{ process: "api", ch: make(chan LogEntry, writerChannelSize), done: make(chan struct{}), } w.enabled.Store(true) go func() { defer close(w.done) for range w.ch { } }() n, err := w.Write([]byte(`{"level":"info","message":"hello","caller":"main.go:10"}`)) assert.NoError(t, err) assert.Greater(t, n, 0) // Give the goroutine a moment, then close close(w.ch) <-w.done } func TestRedisLogWriter_Write_InvalidJSON(t *testing.T) { w := &RedisLogWriter{ process: "api", ch: make(chan LogEntry, writerChannelSize), done: make(chan struct{}), } w.enabled.Store(true) go func() { defer close(w.done) for range w.ch { } }() // Non-JSON input should be silently skipped n, err := w.Write([]byte("not json at all")) assert.NoError(t, err) assert.Greater(t, n, 0) assert.Equal(t, 0, len(w.ch)) close(w.ch) <-w.done } func TestRedisLogWriter_SetEnabled_IsEnabled(t *testing.T) { w := &RedisLogWriter{ process: "api", ch: make(chan LogEntry, 1), done: make(chan struct{}), } w.enabled.Store(true) assert.True(t, w.IsEnabled()) w.SetEnabled(false) assert.False(t, w.IsEnabled()) w.SetEnabled(true) assert.True(t, w.IsEnabled()) } func TestCollector_Stop_MultipleCallsSafe(t *testing.T) { c := &Collector{ process: "api", startTime: time.Now(), stopChan: make(chan struct{}), } // Should not panic on multiple calls c.Stop() c.Stop() c.Stop() }