Files
honeyDueAPI/internal/monitoring/monitoring_test.go
Trey T bec880886b Coverage priorities 1-5: test pure functions, extract interfaces, mock-based handler tests
- Priority 1: Test NewSendEmailTask + NewSendPushTask (5 tests)
- Priority 2: Test customHTTPErrorHandler — all 15+ branches (21 tests)
- Priority 3: Extract Enqueuer interface + payload builders in worker pkg (5 tests)
- Priority 4: Extract ClassifyFile/ComputeRelPath in migrate-encrypt (6 tests)
- Priority 5: Define Handler interfaces, refactor to accept them, mock-based tests (14 tests)
- Fix .gitignore: /worker instead of worker to stop ignoring internal/worker/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:30:09 -05:00

234 lines
5.6 KiB
Go

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()
}