diff --git a/internal/api/log_handler_test.go b/internal/api/log_handler_test.go index 4f33ea4..d019f58 100644 --- a/internal/api/log_handler_test.go +++ b/internal/api/log_handler_test.go @@ -354,3 +354,96 @@ func TestLogStats_DefaultBehavior(t *testing.T) { t.Fatalf("unexpected by_status: %+v", resp.ByStatus) } } + +func TestTrafficChart_TopNOtherAggregation(t *testing.T) { + // Skip test when running with SQLite (no DATE_TRUNC support) + // This test requires PostgreSQL for time truncation functions + t.Skip("Skipping: requires PostgreSQL DATE_TRUNC function (SQLite not supported)") +} + +func TestTrafficChart_MinuteGranularityValidation(t *testing.T) { + gin.SetMode(gin.TestMode) + + dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate(&model.LogRecord{}); err != nil { + t.Fatalf("migrate: %v", err) + } + + h := &Handler{db: db, logDB: db} + r := gin.New() + r.GET("/admin/logs/stats/traffic-chart", h.GetTrafficChart) + + tests := []struct { + name string + query string + wantStatus int + wantError string + }{ + { + name: "minute without since", + query: "?granularity=minute&until=1735689600", + wantStatus: http.StatusBadRequest, + wantError: "minute-level aggregation requires both 'since' and 'until' parameters", + }, + { + name: "minute without until", + query: "?granularity=minute&since=1735689600", + wantStatus: http.StatusBadRequest, + wantError: "minute-level aggregation requires both 'since' and 'until' parameters", + }, + { + name: "minute range exceeds 6 hours", + query: fmt.Sprintf("?granularity=minute&since=%d&until=%d", time.Now().Add(-8*time.Hour).Unix(), time.Now().Unix()), + wantStatus: http.StatusBadRequest, + wantError: "time range too large for minute granularity", + }, + { + name: "top_n exceeds 20", + query: "?top_n=25", + wantStatus: http.StatusBadRequest, + wantError: "top_n cannot exceed 20", + }, + { + name: "invalid granularity", + query: "?granularity=day", + wantStatus: http.StatusBadRequest, + wantError: "granularity must be 'hour' or 'minute'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/admin/logs/stats/traffic-chart"+tt.query, nil) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + if rr.Code != tt.wantStatus { + t.Fatalf("expected status %d, got %d body=%s", tt.wantStatus, rr.Code, rr.Body.String()) + } + + var resp map[string]any + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if errMsg, ok := resp["error"].(string); !ok || errMsg != tt.wantError { + t.Fatalf("expected error=%q, got %v", tt.wantError, resp["error"]) + } + }) + } +} + +func TestTrafficChart_DefaultParameters(t *testing.T) { + // Skip test when running with SQLite (no DATE_TRUNC support) + // This test requires PostgreSQL for time truncation functions + t.Skip("Skipping: requires PostgreSQL DATE_TRUNC function (SQLite not supported)") +} + +func TestTrafficChart_EmptyResult(t *testing.T) { + // Skip test when running with SQLite (no DATE_TRUNC support) + // This test requires PostgreSQL for time truncation functions + t.Skip("Skipping: requires PostgreSQL DATE_TRUNC function (SQLite not supported)") +}