package handlers import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "testing" "time" "Noooste/garage-ui/internal/models" "Noooste/garage-ui/internal/services/mocks" "github.com/gofiber/fiber/v3" ) func newMonitoringTestApp(t *testing.T) (*fiber.App, *mocks.AdminMock) { t.Helper() admin := &mocks.AdminMock{} h := NewMonitoringHandler(admin, nil) // s3Service unused by this handler app := fiber.New() app.Get("/monitoring/metrics", h.GetMetrics) app.Get("/monitoring/admin-health", h.CheckAdminHealth) app.Get("/monitoring/dashboard", h.GetDashboardMetrics) return app, admin } func TestMonitoring_GetMetrics_PassesThroughAsPlainText(t *testing.T) { app, admin := newMonitoringTestApp(t) admin.GetMetricsFn = func(_ context.Context) (string, error) { return "# HELP foo\nfoo 1\n", nil } req := httptest.NewRequest(http.MethodGet, "/monitoring/metrics", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } if ct := resp.Header.Get("Content-Type"); ct != "text/plain; charset=utf-8" { t.Errorf("Content-Type = %q", ct) } b, _ := io.ReadAll(resp.Body) if string(b) != "# HELP foo\nfoo 1\n" { t.Errorf("body = %q", b) } } func TestMonitoring_GetMetrics_ServiceErrorReturns500JSON(t *testing.T) { app, admin := newMonitoringTestApp(t) admin.GetMetricsFn = func(_ context.Context) (string, error) { return "", errors.New("scrape failed") } req := httptest.NewRequest(http.MethodGet, "/monitoring/metrics", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", resp.StatusCode) } } func TestMonitoring_CheckAdminHealth_Healthy(t *testing.T) { app, admin := newMonitoringTestApp(t) admin.HealthCheckFn = func(_ context.Context) error { return nil } req := httptest.NewRequest(http.MethodGet, "/monitoring/admin-health", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } var body struct { Success bool `json:"success"` Data struct { Status string `json:"status"` Message string `json:"message"` } `json:"data"` } _ = json.NewDecoder(resp.Body).Decode(&body) if !body.Success || body.Data.Status != "healthy" { t.Errorf("body = %+v", body) } } func TestMonitoring_CheckAdminHealth_Unhealthy503(t *testing.T) { app, admin := newMonitoringTestApp(t) admin.HealthCheckFn = func(_ context.Context) error { return errors.New("down") } req := httptest.NewRequest(http.MethodGet, "/monitoring/admin-health", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("status = %d, want 503", resp.StatusCode) } } func TestMonitoring_GetDashboardMetrics_AggregatesSizesAndPercentages(t *testing.T) { app, admin := newMonitoringTestApp(t) admin.ListBucketsFn = func(_ context.Context) ([]models.ListBucketsResponseItem, error) { return []models.ListBucketsResponseItem{ {ID: "b1", Created: time.Unix(0, 0), GlobalAliases: []string{"alpha"}}, {ID: "b2", Created: time.Unix(0, 0), GlobalAliases: []string{"beta"}}, }, nil } admin.GetBucketInfoFn = func(_ context.Context, id string) (*models.GarageBucketInfo, error) { switch id { case "b1": return &models.GarageBucketInfo{ID: "b1", Bytes: 300, Objects: 3}, nil case "b2": return &models.GarageBucketInfo{ID: "b2", Bytes: 100, Objects: 1}, nil } return nil, errors.New("unexpected id: " + id) } req := httptest.NewRequest(http.MethodGet, "/monitoring/dashboard", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } var body struct { Data models.DashboardMetrics `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { t.Fatalf("decode: %v", err) } if body.Data.TotalSize != 400 { t.Errorf("TotalSize = %d, want 400", body.Data.TotalSize) } if body.Data.ObjectCount != 4 { t.Errorf("ObjectCount = %d, want 4", body.Data.ObjectCount) } if body.Data.BucketCount != 2 { t.Errorf("BucketCount = %d, want 2", body.Data.BucketCount) } if len(body.Data.UsageByBucket) != 2 { t.Fatalf("UsageByBucket len = %d, want 2", len(body.Data.UsageByBucket)) } // Percentages: 300/400 = 75, 100/400 = 25. for _, u := range body.Data.UsageByBucket { if u.BucketName == "alpha" && u.Percentage != 75 { t.Errorf("alpha pct = %v, want 75", u.Percentage) } if u.BucketName == "beta" && u.Percentage != 25 { t.Errorf("beta pct = %v, want 25", u.Percentage) } } } func TestMonitoring_GetDashboardMetrics_SkipsInaccessibleBuckets(t *testing.T) { app, admin := newMonitoringTestApp(t) admin.ListBucketsFn = func(_ context.Context) ([]models.ListBucketsResponseItem, error) { return []models.ListBucketsResponseItem{ {ID: "good", GlobalAliases: []string{"good"}}, {ID: "bad", GlobalAliases: []string{"bad"}}, }, nil } admin.GetBucketInfoFn = func(_ context.Context, id string) (*models.GarageBucketInfo, error) { if id == "bad" { return nil, errors.New("access denied") } return &models.GarageBucketInfo{ID: "good", Bytes: 10, Objects: 1}, nil } req := httptest.NewRequest(http.MethodGet, "/monitoring/dashboard", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200", resp.StatusCode) } var body struct { Data models.DashboardMetrics `json:"data"` } _ = json.NewDecoder(resp.Body).Decode(&body) if body.Data.BucketCount != 2 { t.Errorf("BucketCount = %d, want 2 (count includes inaccessible)", body.Data.BucketCount) } if len(body.Data.UsageByBucket) != 1 { t.Errorf("UsageByBucket = %d, want 1 (inaccessible skipped)", len(body.Data.UsageByBucket)) } } func TestMonitoring_GetDashboardMetrics_ListBucketsErrorReturns500(t *testing.T) { app, admin := newMonitoringTestApp(t) admin.ListBucketsFn = func(_ context.Context) ([]models.ListBucketsResponseItem, error) { return nil, errors.New("upstream") } req := httptest.NewRequest(http.MethodGet, "/monitoring/dashboard", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusInternalServerError { t.Fatalf("status = %d, want 500", resp.StatusCode) } }