package handlers import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "building-mgmt/internal/models" "building-mgmt/internal/repository" ) // Наборы — чистые функции разбора query, поэтому здесь живут и тестируются // без БД и без роутера: контексту достаточно Request с query-строкой. func newQueryContext(t *testing.T, rawQuery string) (*gin.Context, *httptest.ResponseRecorder) { t.Helper() gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/list?"+rawQuery, nil) return c, w } // errorBody — {"error": ...} из ответа для сообщений об ошибках. func errorBody(t *testing.T, w *httptest.ResponseRecorder) string { t.Helper() var body struct { Error string `json:"error"` } if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { t.Fatalf("разбор ответа %q: %v", w.Body.String(), err) } return body.Error } func TestParsePageDefaults(t *testing.T) { c, w := newQueryContext(t, "") page, ok := parsePage(c, repository.TicketSorts) if !ok { t.Fatalf("ok = false, не должно быть ошибки; ответ: %s", w.Body.String()) } if page.Limit != repository.DefaultLimit || page.Offset != 0 || page.Sort != "" || page.Order != "" { t.Errorf("page = %+v, want дефолт limit=%d", page, repository.DefaultLimit) } } func TestParsePageValid(t *testing.T) { c, _ := newQueryContext(t, "limit=100&offset=50&sort=priority&order=asc") page, ok := parsePage(c, repository.TicketSorts) if !ok { t.Fatal("ok = false, валидные параметры не должны падать") } if page.Limit != 100 || page.Offset != 50 || page.Sort != "priority" || page.Order != "asc" { t.Errorf("page = %+v", page) } } func TestParsePageRejects(t *testing.T) { cases := []struct { name string query string want string }{ {"лимит нулевой", "limit=0", "invalid limit"}, {"лимит за потолком", "limit=101", "invalid limit"}, {"лимит не число", "limit=abc", "invalid limit"}, {"лимит отрицательный", "limit=-5", "invalid limit"}, {"сдвиг отрицательный", "offset=-1", "invalid offset"}, {"сдвиг не число", "offset=1.5", "invalid offset"}, {"порядок не из списка", "order=up", "invalid order"}, {"сортировка не из списка", "sort=password_hash", "invalid sort"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { c, w := newQueryContext(t, tc.query) if _, ok := parsePage(c, repository.TicketSorts); ok { t.Fatal("ok = true, ожидался отказ") } if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } if got := errorBody(t, w); got != tc.want { t.Errorf("error = %q, want %q", got, tc.want) } }) } } func TestCheckFilter(t *testing.T) { t.Run("подходит", func(t *testing.T) { c, _ := newQueryContext(t, "status=closed") if !checkFilter(c, "status", models.TicketStatuses) { t.Error("ok = false, допустимое значение отклонено") } }) t.Run("параметр не задан", func(t *testing.T) { c, _ := newQueryContext(t, "") if !checkFilter(c, "status", models.TicketStatuses) { t.Error("ok = false, пустой фильтр должен проходить") } }) t.Run("мусор", func(t *testing.T) { c, w := newQueryContext(t, "status=nope") if checkFilter(c, "status", models.TicketStatuses) { t.Fatal("ok = true, мусорное значение должно отклоняться") } if w.Code != http.StatusBadRequest || errorBody(t, w) != "invalid status" { t.Errorf("status=%d error=%q, want 400/invalid status", w.Code, errorBody(t, w)) } }) } func TestDateRange(t *testing.T) { t.Run("оба пусты", func(t *testing.T) { c, _ := newQueryContext(t, "") from, to, ok := dateRange(c) if !ok || from != nil || to != nil { t.Errorf("from=%v to=%v ok=%v, want nil/nil/true", from, to, ok) } }) t.Run("date_to включает весь свой день", func(t *testing.T) { c, _ := newQueryContext(t, "date_from=2026-10-01&date_to=2026-10-01") from, to, ok := dateRange(c) if !ok { t.Fatal("ok = false") } wantFrom := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) if !from.Equal(wantFrom) { t.Errorf("from = %v, want %v", from, wantFrom) } // Верхняя граница исключительная — сутки после date_to. if want := wantFrom.AddDate(0, 0, 1); !to.Equal(want) { t.Errorf("to = %v, want %v (весь день целиком)", to, want) } }) t.Run("формат не тот", func(t *testing.T) { c, w := newQueryContext(t, "date_from=01.10.2026") if _, _, ok := dateRange(c); ok { t.Fatal("ok = false ожидался") } if w.Code != http.StatusBadRequest || errorBody(t, w) != "invalid date_from" { t.Errorf("status=%d error=%q", w.Code, errorBody(t, w)) } }) t.Run("диапазон наоборот", func(t *testing.T) { c, w := newQueryContext(t, "date_from=2026-10-05&date_to=2026-10-01") if _, _, ok := dateRange(c); ok { t.Fatal("ok = false ожидался") } if w.Code != http.StatusBadRequest || errorBody(t, w) != "invalid date range" { t.Errorf("status=%d error=%q", w.Code, errorBody(t, w)) } }) }