package metrics import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/glebarez/sqlite" "gorm.io/gorm" "chat/src/auth" "chat/src/db" "chat/src/ws" ) func TestHandleHealth(t *testing.T) { handler := HandleHealth() req := httptest.NewRequest("GET", "/health", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } body := w.Body.String() if !strings.Contains(body, `"healthy"`) { t.Errorf("body missing healthy: %s", body) } if !strings.Contains(body, `"chat"`) { t.Errorf("body missing service name: %s", body) } } func TestHandleNotFound(t *testing.T) { handler := HandleNotFound() req := httptest.NewRequest("GET", "/nonexistent", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusNotFound { t.Errorf("status = %d, want 404", w.Code) } body := w.Body.String() if !strings.Contains(body, `"not found"`) { t.Errorf("body missing not found: %s", body) } } func TestHandleMetrics(t *testing.T) { chatRequestDurationHistogram = newRequestDurationHistogram(requestDurationBuckets) ObserveRequestDuration(100 * time.Millisecond) ObserveRequestDuration(750 * time.Millisecond) gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("failed to open test DB: %v", err) } gormDB.AutoMigrate(&db.Message{}, &db.ModerationLog{}) // Add some test data gormDB.Create(&db.Message{Room: "main", Username: "u1", Text: "hi", Timestamp: time.Now(), Type: "public"}) gormDB.Create(&db.ModerationLog{Action: "mute", Target: "u2", Moderator: "admin", Reason: "spam", Timestamp: time.Now()}) sessionStore := auth.NewSessionStore() hub := ws.NewHub(gormDB, nil, "test-instance", 10, 10*time.Second, "admin") hub.DMsSent = 5 handler := HandleMetrics(gormDB, nil, hub, sessionStore, "sqlite", "test-instance") req := httptest.NewRequest("GET", "/metrics", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } body := w.Body.String() // Check required metric names requiredMetrics := []string{ "linearvoid_chat_connected_clients", "linearvoid_chat_messages_total", "linearvoid_chat_dms_total", "linearvoid_chat_moderation_actions_total", "linearvoid_chat_redis_enabled", "linearvoid_chat_redis_up", "linearvoid_chat_redis_connected_clients", "linearvoid_chat_redis_memory_used_bytes", "linearvoid_chat_redis_memory_max_bytes", "linearvoid_chat_redis_memory_usage_ratio", "linearvoid_chat_admin_sessions_active", "linearvoid_chat_build_info", "linearvoid_chat_request_duration_seconds", } for _, metric := range requiredMetrics { if !strings.Contains(body, metric) { t.Errorf("body missing metric %s", metric) } } // Check build_info has instance_id label if !strings.Contains(body, `instance_id="test-instance"`) { t.Error("build_info missing instance_id label") } // Check values if !strings.Contains(body, "linearvoid_chat_messages_total 1") { t.Error("messages_total should be 1") } if !strings.Contains(body, "linearvoid_chat_moderation_actions_total 1") { t.Error("moderation_actions_total should be 1") } if !strings.Contains(body, "linearvoid_chat_dms_total 5") { t.Error("dms_total should be 5") } if !strings.Contains(body, "linearvoid_chat_redis_enabled 0") { t.Error("redis_enabled should be 0 (nil redis)") } if !strings.Contains(body, "linearvoid_chat_request_duration_seconds_bucket{le=\"1\"} 2") { t.Error("request duration histogram should include both observed samples by the 1s bucket") } if !strings.Contains(body, "linearvoid_chat_request_duration_seconds_count 2") { t.Error("request duration count should be 2") } } func TestHandleMetrics_WrongMethod(t *testing.T) { handler := HandleMetrics(nil, nil, nil, nil, "", "") req := httptest.NewRequest("POST", "/metrics", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("status = %d, want 405", w.Code) } } func TestHandleMetrics_WithRedis(t *testing.T) { chatRequestDurationHistogram = newRequestDurationHistogram(requestDurationBuckets) ObserveRequestDuration(250 * time.Millisecond) gormDB, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) gormDB.AutoMigrate(&db.Message{}, &db.ModerationLog{}) // mockRedis for metrics test mr := &metricsMockRedis{} sessionStore := auth.NewSessionStore() hub := ws.NewHub(gormDB, mr, "test-instance", 10, 10*time.Second, "admin") handler := HandleMetrics(gormDB, mr, hub, sessionStore, "sqlite", "inst-123") req := httptest.NewRequest("GET", "/metrics", nil) w := httptest.NewRecorder() handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } body := w.Body.String() if !strings.Contains(body, "linearvoid_chat_redis_enabled 1") { t.Error("redis_enabled should be 1") } if !strings.Contains(body, "linearvoid_chat_redis_up 1") { t.Error("redis_up should be 1") } if !strings.Contains(body, "linearvoid_chat_redis_connected_clients 5") { t.Error("redis_connected_clients should be 5") } if !strings.Contains(body, "linearvoid_chat_redis_memory_used_bytes 1048576") { t.Error("redis_memory_used_bytes should be 1048576") } if !strings.Contains(body, "linearvoid_chat_redis_memory_usage_ratio 1") { t.Error("redis_memory_usage_ratio should be 1 when maxmemory is unavailable or equal to used memory in tests") } if !strings.Contains(body, "linearvoid_chat_request_duration_seconds_count 1") { t.Error("request duration count should be 1") } } // metricsMockRedis is a minimal mock for metrics tests. type metricsMockRedis struct{} func (m *metricsMockRedis) Ping(ctx context.Context) error { return nil } func (m *metricsMockRedis) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { return nil } func (m *metricsMockRedis) Get(ctx context.Context, key string) (string, error) { return "", nil } func (m *metricsMockRedis) Del(ctx context.Context, keys ...string) error { return nil } func (m *metricsMockRedis) Incr(ctx context.Context, key string) (int64, error) { return 0, nil } func (m *metricsMockRedis) Decr(ctx context.Context, key string) (int64, error) { return 0, nil } func (m *metricsMockRedis) Expire(ctx context.Context, key string, expiration time.Duration) error { return nil } func (m *metricsMockRedis) Exists(ctx context.Context, keys ...string) (int64, error) { return 0, nil } func (m *metricsMockRedis) Publish(ctx context.Context, channel string, message interface{}) error { return nil } func (m *metricsMockRedis) PSubscribe(ctx context.Context, channels ...string) db.PubSub { return nil } func (m *metricsMockRedis) Scan(ctx context.Context, cursor uint64, match string, count int64) ([]string, uint64, error) { return nil, 0, nil } func (m *metricsMockRedis) Info(ctx context.Context, sections ...string) (string, error) { return "connected_clients:5\nused_memory_human:1M\nmaxmemory_human:1M\n", nil } func (m *metricsMockRedis) Close() error { return nil }