168 lines
5.5 KiB
Go
168 lines
5.5 KiB
Go
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))
|
||
}
|
||
})
|
||
}
|