axe/internal/repository/page_test.go
2026-10-18 22:07:25 +03:00

306 lines
9.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"errors"
"testing"
"time"
"building-mgmt/internal/models"
)
// --- чистые функции (база не нужна) ---
func TestPageNormalize(t *testing.T) {
cases := []struct {
name string
in Page
want Page
}{
{"дефолт", Page{}, Page{Limit: DefaultLimit}},
{"нормальный лимит", Page{Limit: 10}, Page{Limit: 10}},
{"лимит за потолком", Page{Limit: 500}, Page{Limit: DefaultLimit}},
{"нулевой лимит", Page{Limit: 0}, Page{Limit: DefaultLimit}},
{"отрицательный сдвиг", Page{Limit: 5, Offset: -3}, Page{Limit: 5, Offset: 0}},
{"порядок сохраняется", Page{Limit: 5, Order: "asc"}, Page{Limit: 5, Order: "asc"}},
{"мусорный порядок сбрасывается", Page{Limit: 5, Order: "up"}, Page{Limit: 5}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := tc.in.normalize()
if got != tc.want {
t.Errorf("normalize(%+v) = %+v, want %+v", tc.in, got, tc.want)
}
})
}
}
func TestOrderBy(t *testing.T) {
// Словарь с ключами по умолчанию: created_at — по убыванию, name — по возрастанию.
sorts := map[string]SortSpec{
"created_at": {Col: "t.created_at", Desc: true},
"name": {Col: "t.name", Desc: false},
}
t.Run("дефолт списка", func(t *testing.T) {
got, err := orderBy(Page{}, sorts, "created_at", "t.id DESC")
if err != nil {
t.Fatalf("err = %v, want nil", err)
}
if want := " ORDER BY t.created_at DESC, t.id DESC"; got != want {
t.Errorf("got %q, want %q", got, want)
}
})
t.Run("направление из запроса", func(t *testing.T) {
got, err := orderBy(Page{Sort: "name", Order: "asc"}, sorts, "created_at", "t.id DESC")
if err != nil {
t.Fatalf("err = %v, want nil", err)
}
if want := " ORDER BY t.name ASC, t.id DESC"; got != want {
t.Errorf("got %q, want %q", got, want)
}
})
t.Run("desc переопределяет дефолт", func(t *testing.T) {
got, err := orderBy(Page{Sort: "name", Order: "desc"}, sorts, "created_at", "t.id DESC")
if err != nil {
t.Fatalf("err = %v, want nil", err)
}
if want := " ORDER BY t.name DESC, t.id DESC"; got != want {
t.Errorf("got %q, want %q", got, want)
}
})
t.Run("неизвестный ключ — ошибка, а не дефолт", func(t *testing.T) {
if _, err := orderBy(Page{Sort: "password_hash"}, sorts, "created_at", "t.id"); !errors.Is(err, ErrInvalid) {
t.Errorf("err = %v, want ErrInvalid", err)
}
})
t.Run("мусорное направление — ошибка", func(t *testing.T) {
if _, err := orderBy(Page{Sort: "name", Order: "sideways"}, sorts, "created_at", "t.id"); !errors.Is(err, ErrInvalid) {
t.Errorf("err = %v, want ErrInvalid", err)
}
})
}
// --- интеграционные (нужна живая БД, без DATABASE_URL скипаются) ---
// TestTicketListPagination — страницы, total и фильтры по полям заявки.
// Ключевое: total считается до обрезки, а offset за концом не ошибка,
// а пустая страница с прежним total.
func TestTicketListPagination(t *testing.T) {
q, ctx := newTestQuerier(t)
tickets := NewTicketRepo(q)
client := createProfile(t, ctx, q, "client")
projectID, objectID, address := createProjectObject(t, ctx, q, client.ID)
// Пять заявок: категории и приоритеты чередуются, чтобы проверить
// сортировку и фильтры независимо от порядка вставки.
specs := []struct{ category, priority string }{
{"plumbing", "high"},
{"electric", "normal"},
{"plumbing", "low"},
{"climate", "high"},
{"plumbing", "normal"},
}
var created int
for _, s := range specs {
tk := &models.Ticket{
ClientID: client.ID,
ProjectID: projectID,
ObjectID: objectID,
Address: address,
Category: s.category,
Description: "описание " + s.category,
Priority: s.priority,
}
if err := tickets.Create(ctx, tk); err != nil {
t.Fatalf("create ticket: %v", err)
}
created++
}
if created != len(specs) {
t.Fatalf("создано %d заявок, want %d", created, len(specs))
}
t.Run("total считается до обрезки", func(t *testing.T) {
items, total, err := tickets.List(ctx, ListFilter{ClientID: client.ID, Page: Page{Limit: 2}})
if err != nil {
t.Fatalf("list: %v", err)
}
if len(items) != 2 || total != 5 {
t.Errorf("len=%d total=%d, want 2 и 5", len(items), total)
}
})
t.Run("страницы идут подряд без пересечений", func(t *testing.T) {
seen := map[int64]bool{}
offset := 0
for page := 1; page <= 3; page++ {
items, total, err := tickets.List(ctx, ListFilter{
ClientID: client.ID,
Page: Page{Limit: 2, Offset: offset},
})
if err != nil {
t.Fatalf("page %d: %v", page, err)
}
if total != 5 {
t.Errorf("page %d total = %d, want 5", page, total)
}
if page < 3 && len(items) != 2 {
t.Errorf("page %d len = %d, want 2", page, len(items))
}
if page == 3 && len(items) != 1 {
t.Errorf("page 3 len = %d, want 1", len(items))
}
for _, tk := range items {
if seen[tk.ID] {
t.Errorf("ticket %d выдан дважды", tk.ID)
}
seen[tk.ID] = true
}
offset += 2
}
if len(seen) != 5 {
t.Errorf("уникальных заявок за 3 страницы = %d, want 5", len(seen))
}
})
t.Run("offset за концом — пустая страница, total цел", func(t *testing.T) {
items, total, err := tickets.List(ctx, ListFilter{ClientID: client.ID, Page: Page{Limit: 2, Offset: 100}})
if err != nil {
t.Fatalf("list: %v", err)
}
if len(items) != 0 || total != 5 {
t.Errorf("len=%d total=%d, want 0 и 5", len(items), total)
}
})
t.Run("лимит вне границ подрезается, а не падает", func(t *testing.T) {
items, _, err := tickets.List(ctx, ListFilter{ClientID: client.ID, Page: Page{Limit: 500}})
if err != nil {
t.Fatalf("list: %v", err)
}
if len(items) != 5 {
t.Errorf("len = %d, want 5 (лимит 500 → 100 → все)", len(items))
}
})
t.Run("сортировка по приоритету: сначала важные", func(t *testing.T) {
items, _, err := tickets.List(ctx, ListFilter{
ClientID: client.ID,
Page: Page{Limit: MaxLimit, Sort: "priority", Order: "asc"},
})
if err != nil {
t.Fatalf("list: %v", err)
}
if len(items) != 5 {
t.Fatalf("len = %d, want 5", len(items))
}
if items[0].Priority != "high" || items[len(items)-1].Priority != "low" {
t.Errorf("порядок приоритетов: %v", priorities(items))
}
})
t.Run("неизвестное поле сортировки — ErrInvalid", func(t *testing.T) {
if _, _, err := tickets.List(ctx, ListFilter{Page: Page{Sort: "description"}}); !errors.Is(err, ErrInvalid) {
t.Errorf("err = %v, want ErrInvalid", err)
}
})
t.Run("фильтр по категории", func(t *testing.T) {
items, total, err := tickets.List(ctx, ListFilter{
ClientID: client.ID, Category: "plumbing",
Page: Page{Limit: MaxLimit},
})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 3 || len(items) != 3 {
t.Errorf("total=%d len=%d, want 3 и 3", total, len(items))
}
for _, tk := range items {
if tk.Category != "plumbing" {
t.Errorf("фильтр category пропустил %q", tk.Category)
}
}
})
t.Run("фильтр по приоритету", func(t *testing.T) {
_, total, err := tickets.List(ctx, ListFilter{
ClientID: client.ID, Priority: "high",
Page: Page{Limit: MaxLimit},
})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 2 {
t.Errorf("total = %d, want 2", total)
}
})
t.Run("фильтры комбинируются", func(t *testing.T) {
_, total, err := tickets.List(ctx, ListFilter{
ClientID: client.ID, Category: "plumbing", Priority: "high",
Page: Page{Limit: MaxLimit},
})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 1 {
t.Errorf("total = %d, want 1 (только plumbing+high)", total)
}
})
t.Run("диапазон дат", func(t *testing.T) {
today := time.Now().AddDate(0, 0, -1)
tomorrow := today.AddDate(0, 0, 2)
_, total, err := tickets.List(ctx, ListFilter{
ClientID: client.ID,
DateFrom: &today, DateTo: &tomorrow,
Page: Page{Limit: MaxLimit},
})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 5 {
t.Errorf("диапазон, покрывающий сегодняшний день: total = %d, want 5", total)
}
onlyFuture := tomorrow.AddDate(0, 0, 10)
items, total, err := tickets.List(ctx, ListFilter{
ClientID: client.ID,
DateFrom: &onlyFuture,
Page: Page{Limit: MaxLimit},
})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 0 || len(items) != 0 {
t.Errorf("будущий диапазон: total=%d len=%d, want 0 и 0", total, len(items))
}
})
t.Run("пустая выборка возвращает total=0", func(t *testing.T) {
items, total, err := tickets.List(ctx, ListFilter{ClientID: "00000000-0000-0000-0000-000000000000"})
if err != nil {
t.Fatalf("list: %v", err)
}
if len(items) != 0 || total != 0 {
t.Errorf("len=%d total=%d, want 0 и 0", len(items), total)
}
})
}
// priorities — приоритеты страницы по порядку, для сообщений об ошибках.
func priorities(items []models.Ticket) []string {
out := make([]string, 0, len(items))
for _, tk := range items {
out = append(out, tk.Priority)
}
return out
}