axe/internal/handlers/list_test.go

168 lines
5.5 KiB
Go
Raw Normal View History

2026-10-18 19:07:25 +00:00
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))
}
})
}