- 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>
234 lines
5.6 KiB
Go
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()
|
|
}
|