1000 lines
40 KiB
Go
1000 lines
40 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/valyala/fasthttp"
|
|
|
|
"wucher/internal/domain/base"
|
|
filemanager "wucher/internal/domain/file_manager"
|
|
filemanagerrealtime "wucher/internal/realtime/filemanager"
|
|
"wucher/internal/service"
|
|
"wucher/internal/shared/pkg/apperrors"
|
|
"wucher/internal/shared/pkg/attachmentresolver"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
"wucher/internal/transport/http/jsonapi"
|
|
)
|
|
|
|
var (
|
|
testBaseShiftStart = "06:00:00"
|
|
testBaseShiftEnd = "21:00:00"
|
|
)
|
|
|
|
type memBaseRepo struct {
|
|
rows map[string]map[string]*base.Base
|
|
|
|
createBaseErr error
|
|
updateBaseErr error
|
|
deleteBaseErr error
|
|
getBaseByIDErr error
|
|
listBasesErr error
|
|
|
|
listRows []base.Base
|
|
listTotal int64
|
|
|
|
lastCreateBase *base.Base
|
|
lastCreateCategory string
|
|
lastUpdateBase *base.Base
|
|
lastUpdateCategory string
|
|
lastDeleteID []byte
|
|
lastDeleteCategory string
|
|
lastGetID []byte
|
|
lastGetCategory string
|
|
lastListFilter string
|
|
lastListSort string
|
|
lastListLimit int
|
|
lastListOffset int
|
|
lastListCategory string
|
|
lastListDUL *bool
|
|
}
|
|
|
|
type basePresignStorageMock struct {
|
|
url string
|
|
err error
|
|
}
|
|
|
|
func (m *basePresignStorageMock) PresignGetObject(_ context.Context, _ string, _ time.Duration) (string, error) {
|
|
if m.err != nil {
|
|
return "", m.err
|
|
}
|
|
return m.url, nil
|
|
}
|
|
|
|
func newMemBaseRepo() *memBaseRepo {
|
|
return &memBaseRepo{
|
|
rows: map[string]map[string]*base.Base{
|
|
base.CategoryKeyRegular: {},
|
|
base.CategoryKeyHEMS: {},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *memBaseRepo) CreateBase(_ context.Context, row *base.Base, categoryType string) error {
|
|
if r.createBaseErr != nil {
|
|
return r.createBaseErr
|
|
}
|
|
if len(row.ID) == 0 {
|
|
row.ID = uuidv7.MustBytes()
|
|
}
|
|
r.lastCreateBase = row
|
|
r.lastCreateCategory = categoryType
|
|
if _, ok := r.rows[categoryType]; !ok {
|
|
r.rows[categoryType] = map[string]*base.Base{}
|
|
}
|
|
r.rows[categoryType][string(row.ID)] = row
|
|
return nil
|
|
}
|
|
|
|
func (r *memBaseRepo) UpdateBase(_ context.Context, row *base.Base, categoryType string) error {
|
|
if r.updateBaseErr != nil {
|
|
return r.updateBaseErr
|
|
}
|
|
r.lastUpdateBase = row
|
|
r.lastUpdateCategory = categoryType
|
|
if _, ok := r.rows[categoryType]; !ok {
|
|
r.rows[categoryType] = map[string]*base.Base{}
|
|
}
|
|
r.rows[categoryType][string(row.ID)] = row
|
|
return nil
|
|
}
|
|
|
|
func (r *memBaseRepo) DeleteBase(_ context.Context, id []byte, categoryType string) error {
|
|
if r.deleteBaseErr != nil {
|
|
return r.deleteBaseErr
|
|
}
|
|
r.lastDeleteID = append([]byte(nil), id...)
|
|
r.lastDeleteCategory = categoryType
|
|
if m, ok := r.rows[categoryType]; ok {
|
|
delete(m, string(id))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *memBaseRepo) GetBaseByID(_ context.Context, id []byte, categoryType string) (*base.Base, error) {
|
|
if r.getBaseByIDErr != nil {
|
|
return nil, r.getBaseByIDErr
|
|
}
|
|
r.lastGetID = append([]byte(nil), id...)
|
|
r.lastGetCategory = categoryType
|
|
if m, ok := r.rows[categoryType]; ok {
|
|
return m[string(id)], nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (r *memBaseRepo) ListBases(_ context.Context, filter, sort string, limit, offset int, categoryType string, dul *bool) ([]base.Base, int64, error) {
|
|
if r.listBasesErr != nil {
|
|
return nil, 0, r.listBasesErr
|
|
}
|
|
r.lastListFilter = filter
|
|
r.lastListSort = sort
|
|
r.lastListLimit = limit
|
|
r.lastListOffset = offset
|
|
r.lastListCategory = categoryType
|
|
r.lastListDUL = dul
|
|
if r.listRows != nil {
|
|
return r.listRows, r.listTotal, nil
|
|
}
|
|
out := make([]base.Base, 0)
|
|
for _, row := range r.rows[categoryType] {
|
|
out = append(out, *row)
|
|
}
|
|
return out, int64(len(out)), nil
|
|
}
|
|
|
|
func newBaseTestApp(h *BaseHandler) *fiber.App {
|
|
app := fiber.New()
|
|
app.Post("/api/v1/bases/create", h.CreateBase)
|
|
app.Patch("/api/v1/bases/update/:uuid", h.UpdateBase)
|
|
app.Delete("/api/v1/bases/delete/:uuid", h.DeleteBase)
|
|
app.Get("/api/v1/bases/get/:uuid", h.GetBase)
|
|
app.Get("/api/v1/bases/get-all", h.ListBases)
|
|
app.Get("/api/v1/bases/get-all/dt", h.ListBasesDatatable)
|
|
return app
|
|
}
|
|
|
|
func newBaseEventsTestApp(h *FileManagerEventsHandler, actor []byte) *fiber.App {
|
|
app := fiber.New()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
if actor != nil {
|
|
c.Locals("user_id", append([]byte(nil), actor...))
|
|
}
|
|
return c.Next()
|
|
})
|
|
app.Get("/api/v1/bases/events", h.Stream)
|
|
return app
|
|
}
|
|
|
|
func doJSONRequestBase(t *testing.T, app *fiber.App, method, path string, body []byte) (*http.Response, []byte) {
|
|
t.Helper()
|
|
req, err := http.NewRequest(method, path, bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("new request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("read body: %v", err)
|
|
}
|
|
return resp, respBody
|
|
}
|
|
|
|
func mustUUIDStringBase(t *testing.T, id []byte) string {
|
|
t.Helper()
|
|
s, err := uuidv7.BytesToString(id)
|
|
if err != nil {
|
|
t.Fatalf("uuid to string: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestNewBaseHandler(t *testing.T) {
|
|
h := NewBaseHandler(service.NewBaseService(newMemBaseRepo()))
|
|
if h == nil || h.validate == nil {
|
|
t.Fatalf("expected handler and validator initialized")
|
|
}
|
|
h = h.WithFileManagerService(nil)
|
|
h = h.WithFileResolver(nil, nil)
|
|
if h == nil {
|
|
t.Fatalf("expected chain methods return handler")
|
|
}
|
|
}
|
|
|
|
func TestBaseHandlerCreateBase(t *testing.T) {
|
|
t.Run("invalid category", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, _ := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create?category_type=oops", []byte(`{"data":{"type":"base","attributes":{"base":"A","latitude":47.3769,"longitude":8.5417}}}`))
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("missing coordinates", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, _ := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"No coords"}}}`))
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid coordinates", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, _ := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"Bad coords","latitude":91,"longitude":181}}}`))
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("regular success", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"Regular A","latitude":47.3769,"longitude":8.5417,"default_shift_start":"`+testBaseShiftStart+`","default_shift_end":"`+testBaseShiftEnd+`","sortkey":3,"notes":"Primary note","is_active":false}}}`))
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastCreateCategory != base.CategoryKeyRegular {
|
|
t.Fatalf("expected regular category, got %q", repo.lastCreateCategory)
|
|
}
|
|
if repo.lastCreateBase == nil || repo.lastCreateBase.SortKey == nil || *repo.lastCreateBase.SortKey != 3 {
|
|
t.Fatalf("expected sortkey mapped on create")
|
|
}
|
|
if repo.lastCreateBase.Notes != "Primary note" || repo.lastCreateBase.IsActive {
|
|
t.Fatalf("expected notes/is_active mapped on create")
|
|
}
|
|
if !bytes.Contains(body, []byte(`"type":"base"`)) {
|
|
t.Fatalf("expected base resource type, body=%s", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("operational shift times success", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"Base with ops","latitude":47.3769,"longitude":8.5417,"default_shift_start":"`+testBaseShiftStart+`","default_shift_end":"`+testBaseShiftEnd+`","operational_shift_times":[{"date_start":"2026-04-12","date_end":"2026-04-12","shift_time":{"start":"09:00","end":"18:00"}},{"date_start":"2026-04-13","date_end":"2026-04-13","shift_time":{"start":"09:00","end":"17:00"}}]}}}`))
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastCreateBase == nil || len(repo.lastCreateBase.OperationalShiftTimes) != 2 {
|
|
t.Fatalf("expected operational shift times mapped on create")
|
|
}
|
|
if got := repo.lastCreateBase.OperationalShiftTimes[1].DateStart.Format("2006-01-02"); got != "2026-04-13" {
|
|
t.Fatalf("unexpected operational shift date start: %s", got)
|
|
}
|
|
if got := repo.lastCreateBase.OperationalShiftTimes[1].DateEnd.Format("2006-01-02"); got != "2026-04-13" {
|
|
t.Fatalf("unexpected operational shift date end: %s", got)
|
|
}
|
|
if repo.lastCreateBase.OperationalShiftTimes[0].ShiftStart != "09:00:00" || repo.lastCreateBase.OperationalShiftTimes[1].ShiftEnd != "17:00:00" {
|
|
t.Fatalf("expected both operational shift times mapped on create, got %#v", repo.lastCreateBase.OperationalShiftTimes)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"date_start":"2026-04-12"`)) || !bytes.Contains(body, []byte(`"date_end":"2026-04-12"`)) || !bytes.Contains(body, []byte(`"date_start":"2026-04-13"`)) || !bytes.Contains(body, []byte(`"date_end":"2026-04-13"`)) {
|
|
t.Fatalf("expected operational shift dates in response, body=%s", string(body))
|
|
}
|
|
if !bytes.Contains(body, []byte(`"operational_shift_times":[`)) {
|
|
t.Fatalf("expected operational_shift_times in response, body=%s", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("operational shift times fixed response includes values", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"Base with fixed ops","latitude":47.3769,"longitude":8.5417,"default_shift_start":"`+testBaseShiftStart+`","default_shift_end":"`+testBaseShiftEnd+`","operational_shift_times":[{"date_start":"2026-06-03","date_end":"2026-06-04","start_time_type":"FIXED","end_time_type":"FIXED","shift_time":{"start":"03:00","end":"05:00"}}]}}}`))
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"shift_time":{"start":"03:00","end":"05:00"}`)) {
|
|
t.Fatalf("expected fixed operational shift_time values in response, body=%s", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("operational shift times invalid order", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"Base with bad ops","latitude":47.3769,"longitude":8.5417,"default_shift_start":"`+testBaseShiftStart+`","default_shift_end":"`+testBaseShiftEnd+`","operational_shift_times":[{"date_start":"2026-04-12","date_end":"2026-04-12","shift_time":{"start":"18:00","end":"09:00"}}]}}}`))
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(body, []byte(`shift_time.start must be earlier than shift_time.end`)) {
|
|
t.Fatalf("expected shift order validation error, body=%s", string(body))
|
|
}
|
|
if repo.lastCreateBase != nil {
|
|
t.Fatalf("expected create not to reach repo on invalid shift order")
|
|
}
|
|
})
|
|
|
|
t.Run("hems success", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create?category_type=hems", []byte(`{"data":{"type":"hems_base","attributes":{"base":"HEMS A","latitude":47.3769,"longitude":8.5417,"default_shift_start":"`+testBaseShiftStart+`","default_shift_end":"`+testBaseShiftEnd+`"}}}`))
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastCreateCategory != base.CategoryKeyHEMS {
|
|
t.Fatalf("expected hems category, got %q", repo.lastCreateCategory)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"type":"hems_base"`)) {
|
|
t.Fatalf("expected hems_base resource type, body=%s", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("hems success from body base_category without query", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"HEMS from body","base_category":"hems","latitude":47.3769,"longitude":8.5417,"default_shift_start":"`+testBaseShiftStart+`","default_shift_end":"`+testBaseShiftEnd+`"}}}`))
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastCreateCategory != base.CategoryKeyHEMS {
|
|
t.Fatalf("expected hems category, got %q", repo.lastCreateCategory)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"base_category":"hems"`)) {
|
|
t.Fatalf("expected base_category hems, body=%s", string(body))
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
func TestBaseHandlerGetBase(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
hemsID := uuidv7.MustBytes()
|
|
fotoID := uuidv7.MustBytes()
|
|
repo.rows[base.CategoryKeyHEMS][string(hemsID)] = &base.Base{ID: hemsID, BaseName: "HEMS 1", FotoAttachmentID: fotoID, CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get/"+mustUUIDStringBase(t, hemsID)+"?category_type=hems", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastGetCategory != base.CategoryKeyHEMS {
|
|
t.Fatalf("expected hems category forwarded")
|
|
}
|
|
if !bytes.Contains(body, []byte(`"type":"hems_base"`)) {
|
|
t.Fatalf("expected hems_base type")
|
|
}
|
|
if !bytes.Contains(body, []byte(`"base_category":"hems"`)) {
|
|
t.Fatalf("expected base_category hems")
|
|
}
|
|
fotoIDStr, _ := uuidv7.BytesToString(fotoID)
|
|
bodyStr := string(body)
|
|
if !strings.Contains(bodyStr, `"uuid":"`+fotoIDStr+`"`) {
|
|
t.Fatalf("expected foto uuid when storage unavailable, body=%s", bodyStr)
|
|
}
|
|
if !strings.Contains(bodyStr, `"download_url":""`) {
|
|
t.Fatalf("expected empty download_url when storage unavailable, body=%s", bodyStr)
|
|
}
|
|
}
|
|
|
|
func TestBaseHelperFunctions(t *testing.T) {
|
|
t.Run("cloneIntPointerBase and preferStringPtr", func(t *testing.T) {
|
|
v := 7
|
|
cp := cloneIntPointerBase(&v)
|
|
if cp == nil || *cp != 7 || cp == &v {
|
|
t.Fatalf("cloneIntPointerBase invalid clone")
|
|
}
|
|
if cloneIntPointerBase(nil) != nil {
|
|
t.Fatalf("expected nil clone for nil input")
|
|
}
|
|
a, b := "a", "b"
|
|
if got := preferStringPtr(&a, &b); got == nil || *got != "a" {
|
|
t.Fatalf("preferStringPtr should prioritize primary")
|
|
}
|
|
if got := preferStringPtr(nil, &b); got == nil || *got != "b" {
|
|
t.Fatalf("preferStringPtr should use fallback")
|
|
}
|
|
})
|
|
|
|
t.Run("parseUUIDList and uuidListToStrings", func(t *testing.T) {
|
|
id1 := mustUUIDStringBase(t, uuidv7.MustBytes())
|
|
id2 := mustUUIDStringBase(t, uuidv7.MustBytes())
|
|
got, errObj := parseUUIDList([]string{" ", id1, id1, id2}, "/x")
|
|
if errObj != nil || len(got) != 2 {
|
|
t.Fatalf("expected deduped parsed ids, got len=%d err=%v", len(got), errObj)
|
|
}
|
|
if _, errObj = parseUUIDList([]string{"not-uuid"}, "/x"); errObj == nil {
|
|
t.Fatalf("expected validation error for invalid uuid")
|
|
}
|
|
strs := uuidListToStrings(got)
|
|
if len(strs) != 2 {
|
|
t.Fatalf("uuidListToStrings expected 2 got %d", len(strs))
|
|
}
|
|
if uuidListToStrings(nil) != nil {
|
|
t.Fatalf("uuidListToStrings nil input should be nil")
|
|
}
|
|
})
|
|
|
|
t.Run("baseContactResources", func(t *testing.T) {
|
|
id := uuidv7.MustBytes()
|
|
rows := []base.BaseContactPerson{
|
|
{ContactID: id, FirstName: "A", LastName: "B"},
|
|
{ContactID: []byte{1, 2}, FirstName: "X"},
|
|
}
|
|
out := baseContactResources(rows)
|
|
if len(out) != 1 || out[0].FirstName != "A" {
|
|
t.Fatalf("expected single valid contact resource")
|
|
}
|
|
if baseContactResources(nil) != nil {
|
|
t.Fatalf("nil input should return nil")
|
|
}
|
|
})
|
|
|
|
t.Run("shift parsing helpers", func(t *testing.T) {
|
|
startRaw := "07:30"
|
|
endRaw := "16:45:30"
|
|
start, end, errObj := parseBaseShiftRange(&startRaw, &endRaw, nil, "/s", "/e", "/l")
|
|
if errObj != nil || start == nil || end == nil {
|
|
t.Fatalf("expected valid shift parse")
|
|
}
|
|
legacy := "09:00-18:00"
|
|
start, end, errObj = parseBaseShiftRange(nil, nil, &legacy, "/s", "/e", "/l")
|
|
if errObj != nil || start == nil || end == nil || start.Format("15:04") != "09:00" {
|
|
t.Fatalf("expected legacy shift parse")
|
|
}
|
|
bad := "invalid"
|
|
_, _, errObj = parseBaseShiftRange(nil, nil, &bad, "/s", "/e", "/l")
|
|
if errObj == nil {
|
|
t.Fatalf("expected error for invalid legacy shift")
|
|
}
|
|
if tokens := extractBaseShiftClockTokens("x 01:02:03 y 14:15"); len(tokens) != 2 {
|
|
t.Fatalf("expected two extracted tokens")
|
|
}
|
|
a, b := extractBaseShiftRangeTokens("10:10")
|
|
if a != "10:10" || b != "10:10" {
|
|
t.Fatalf("single token should duplicate")
|
|
}
|
|
})
|
|
|
|
t.Run("query and category helpers", func(t *testing.T) {
|
|
app := fiber.New()
|
|
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
|
defer app.ReleaseCtx(ctx)
|
|
ctx.Request().URI().SetQueryString("dul=true&category_type=hems")
|
|
dul, errObj := parseOptionalBoolQueryParam(ctx, "dul")
|
|
if errObj != nil || dul == nil || !*dul {
|
|
t.Fatalf("expected dul=true parsed")
|
|
}
|
|
ctx.Request().URI().SetQueryString("dul=oops")
|
|
if _, errObj = parseOptionalBoolQueryParam(ctx, "dul"); errObj == nil {
|
|
t.Fatalf("expected bool validation error")
|
|
}
|
|
ctx.Request().URI().SetQueryString("category=hems")
|
|
if got := baseCategoryQueryValue(ctx); got != "hems" {
|
|
t.Fatalf("expected category fallback value")
|
|
}
|
|
if categoryNotFoundDetail(base.CategoryKeyHEMS) != "hems base not found" {
|
|
t.Fatalf("unexpected category detail for hems")
|
|
}
|
|
})
|
|
|
|
t.Run("uuid parser and converter helpers", func(t *testing.T) {
|
|
idStr := mustUUIDStringBase(t, uuidv7.MustBytes())
|
|
if got, errObj := parseUUIDOrClear(idStr, "/p"); errObj != nil || len(got) != 16 {
|
|
t.Fatalf("parseUUIDOrClear should parse valid uuid")
|
|
}
|
|
if got, errObj := parseUUIDOrClear(" ", "/p"); errObj != nil || got != nil {
|
|
t.Fatalf("parseUUIDOrClear should clear blank")
|
|
}
|
|
if _, errObj := parseUUIDOrClear("bad", "/p"); errObj == nil {
|
|
t.Fatalf("expected parseUUIDOrClear error")
|
|
}
|
|
if bytesUUIDToString(nil) != "" {
|
|
t.Fatalf("empty bytes should return empty")
|
|
}
|
|
if bytesUUIDToString([]byte{1, 2}) != "" {
|
|
t.Fatalf("invalid bytes should return empty")
|
|
}
|
|
})
|
|
|
|
t.Run("inferBaseErrorCode branches", func(t *testing.T) {
|
|
cases := []struct {
|
|
status int
|
|
title string
|
|
detail string
|
|
}{
|
|
{fiber.StatusNotFound, "", ""},
|
|
{fiber.StatusConflict, "", ""},
|
|
{fiber.StatusUnprocessableEntity, "Validation error", "invalid uuid"},
|
|
{fiber.StatusUnprocessableEntity, "Validation error", "category_type/base_category must be one of"},
|
|
{fiber.StatusUnprocessableEntity, "Validation error", "time must be HH:MM"},
|
|
{fiber.StatusBadRequest, "Invalid JSON", ""},
|
|
{fiber.StatusBadRequest, "Create failed", ""},
|
|
{fiber.StatusBadRequest, "Update failed", ""},
|
|
{fiber.StatusBadRequest, "Delete failed", ""},
|
|
{fiber.StatusBadRequest, "List failed", ""},
|
|
{fiber.StatusInternalServerError, "", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
code, errCode := inferBaseErrorCode(tc.status, tc.title, tc.detail)
|
|
if strings.TrimSpace(code) == "" || strings.TrimSpace(errCode) == "" {
|
|
t.Fatalf("expected non-empty codes for status=%d title=%q", tc.status, tc.title)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("writeBaseErrors populates status and code", func(t *testing.T) {
|
|
app := fiber.New()
|
|
app.Get("/", func(c *fiber.Ctx) error {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Title: "Invalid JSON", Detail: "oops"}})
|
|
})
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil || resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("writeBaseErrors failed err=%v status=%d", err, resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("base attachment resolver branches", func(t *testing.T) {
|
|
handler := NewBaseHandler(service.NewBaseService(newMemBaseRepo())).WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{})
|
|
baseID := uuidv7.MustBytes()
|
|
app := fiber.New()
|
|
app.Get("/invalid-attachment", func(c *fiber.Ctx) error {
|
|
raw := "bad"
|
|
_, handled := handler.resolveAttachmentIDFromInput(c, attachmentresolver.Params{
|
|
RawAttachmentID: &raw,
|
|
AttachmentField: "attachment_id",
|
|
FileField: "file_uuid",
|
|
}, "Update failed")
|
|
if !handled {
|
|
t.Fatalf("expected handled for invalid attachment")
|
|
}
|
|
return nil
|
|
})
|
|
app.Get("/invalid-file", func(c *fiber.Ctx) error {
|
|
raw := "bad"
|
|
_, handled := handler.resolveAttachmentIDFromInput(c, attachmentresolver.Params{
|
|
RawFileID: &raw,
|
|
AttachmentField: "attachment_id",
|
|
FileField: "file_uuid",
|
|
FileManager: nil,
|
|
}, "Update failed")
|
|
if !handled {
|
|
t.Fatalf("expected handled for invalid file path")
|
|
}
|
|
return nil
|
|
})
|
|
app.Get("/require", func(c *fiber.Ctx) error {
|
|
_, handled := handler.resolveAttachmentIDFromInput(c, attachmentresolver.Params{
|
|
RequireValue: true,
|
|
AttachmentField: "attachment_id",
|
|
FileField: "file_uuid",
|
|
}, "Update failed")
|
|
if !handled {
|
|
t.Fatalf("expected handled for require value")
|
|
}
|
|
return nil
|
|
})
|
|
app.Get("/not-found", func(c *fiber.Ctx) error {
|
|
id := mustUUIDStringBase(t, uuidv7.MustBytes())
|
|
_, handled := handler.resolveAttachmentIDFromInput(c, attachmentresolver.Params{
|
|
RawAttachmentID: &id,
|
|
AttachmentField: "attachment_id",
|
|
FileField: "file_uuid",
|
|
ValidateAttachmentExists: true,
|
|
AttachmentExists: func(context.Context, []byte) (bool, error) {
|
|
return false, nil
|
|
},
|
|
}, "Update failed")
|
|
if !handled {
|
|
t.Fatalf("expected handled for attachment not found")
|
|
}
|
|
return nil
|
|
})
|
|
app.Get("/existing-or-file", func(c *fiber.Ctx) error {
|
|
raw := mustUUIDStringBase(t, uuidv7.MustBytes())
|
|
mock := &fileManagerAttachmentHandlerServiceMock{
|
|
getAttachmentFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
|
|
return &filemanager.Attachment{ID: uuidv7.MustBytes()}, nil
|
|
},
|
|
}
|
|
h2 := NewBaseHandler(service.NewBaseService(newMemBaseRepo())).WithFileManagerService(mock)
|
|
id, handled := h2.resolveExistingBaseFotoAttachmentOrFileID(c, baseID, &raw)
|
|
if handled || len(id) != 16 {
|
|
t.Fatalf("expected existing attachment id")
|
|
}
|
|
return nil
|
|
})
|
|
app.Get("/existing-error", func(c *fiber.Ctx) error {
|
|
raw := mustUUIDStringBase(t, uuidv7.MustBytes())
|
|
mock := &fileManagerAttachmentHandlerServiceMock{
|
|
getAttachmentFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
|
|
return nil, errors.New("lookup failed")
|
|
},
|
|
}
|
|
h2 := NewBaseHandler(service.NewBaseService(newMemBaseRepo())).WithFileManagerService(mock)
|
|
_, handled := h2.resolveExistingBaseFotoAttachmentOrFileID(c, baseID, &raw)
|
|
if !handled {
|
|
t.Fatalf("expected handled error for existing attachment lookup")
|
|
}
|
|
return nil
|
|
})
|
|
app.Get("/resolve-base-foto", func(c *fiber.Ctx) error {
|
|
_, handled := handler.resolveBaseFotoAttachmentID(c, baseID, nil)
|
|
if handled {
|
|
t.Fatalf("nil raw should not be handled as error")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
for _, path := range []string{"/invalid-attachment", "/invalid-file", "/require", "/not-found", "/existing-or-file", "/existing-error", "/resolve-base-foto"} {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("request %s err=%v", path, err)
|
|
}
|
|
if path != "/existing-or-file" && path != "/resolve-base-foto" && resp.StatusCode == http.StatusOK {
|
|
t.Fatalf("expected error status for %s", path)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBaseHandlerGetBase_UsesDirectPresignURLWhenStorageAvailable(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
hemsID := uuidv7.MustBytes()
|
|
fotoID := uuidv7.MustBytes()
|
|
repo.rows[base.CategoryKeyHEMS][string(hemsID)] = &base.Base{
|
|
ID: hemsID,
|
|
BaseName: "HEMS 1",
|
|
FotoAttachmentID: fotoID,
|
|
FotoAttachment: &filemanager.Attachment{
|
|
ID: fotoID,
|
|
FileID: uuidv7.MustBytes(),
|
|
File: &filemanager.File{ID: uuidv7.MustBytes(), ObjectKey: "base/hems-1.jpg"},
|
|
},
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
h := NewBaseHandler(service.NewBaseService(repo)).WithFileStorage(
|
|
&basePresignStorageMock{url: "https://signed.example/base/hems-1.jpg?sig=abc"},
|
|
)
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get/"+mustUUIDStringBase(t, hemsID)+"?category_type=hems", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"download_url":"https://signed.example/base/hems-1.jpg?sig=abc"`)) {
|
|
t.Fatalf("expected direct presigned URL, body=%s", string(body))
|
|
}
|
|
}
|
|
|
|
func TestBaseHandlerUpdateDelete(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[base.CategoryKeyHEMS][string(id)] = &base.Base{ID: id, BaseName: "Old", DefaultShiftStart: "06:00:00", DefaultShiftEnd: "21:00:00", CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
idStr := mustUUIDStringBase(t, id)
|
|
|
|
resp, _ := doJSONRequestBase(t, app, http.MethodPatch, "/api/v1/bases/update/"+idStr+"?category_type=hems", []byte(`{"data":{"type":"hems_base","id":"`+idStr+`","attributes":{"base":"New","latitude":47.3769,"longitude":8.5417}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastUpdateCategory != base.CategoryKeyHEMS {
|
|
t.Fatalf("expected hems category update")
|
|
}
|
|
|
|
fotoID := uuidv7.MustBytes()
|
|
repo.rows[base.CategoryKeyHEMS][string(id)].FotoAttachmentID = append([]byte(nil), fotoID...)
|
|
resp, _ = doJSONRequestBase(t, app, http.MethodPatch, "/api/v1/bases/update/"+idStr+"?category_type=hems", []byte(`{"data":{"type":"hems_base","id":"`+idStr+`","attributes":{"base":"Still New","latitude":47.3769,"longitude":8.5417,"file_uuid":""}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.rows[base.CategoryKeyHEMS][string(id)].FotoAttachmentID != nil {
|
|
t.Fatalf("expected foto attachment to be cleared")
|
|
}
|
|
|
|
resp, updateBody := doJSONRequestBase(t, app, http.MethodPatch, "/api/v1/bases/update/"+idStr+"?category_type=hems", []byte(`{"data":{"type":"hems_base","id":"`+idStr+`","attributes":{"base":"With Ops","latitude":47.3769,"longitude":8.5417,"operational_shift_times":[{"date_start":"2026-04-12","date_end":"2026-04-12","shift_time":{"start":"09:00","end":"18:00"}},{"date_start":"2026-04-13","date_end":"2026-04-13","shift_time":{"start":"09:00","end":"17:00"}}]}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastUpdateBase == nil || len(repo.lastUpdateBase.OperationalShiftTimes) != 2 {
|
|
t.Fatalf("expected operational shift times mapped on update")
|
|
}
|
|
if repo.lastUpdateBase.OperationalShiftTimes[0].ShiftStart != "09:00:00" || repo.lastUpdateBase.OperationalShiftTimes[1].ShiftEnd != "17:00:00" {
|
|
t.Fatalf("expected operational shift mapping on update, got %#v", repo.lastUpdateBase.OperationalShiftTimes)
|
|
}
|
|
if !bytes.Contains(updateBody, []byte(`"date_start":"2026-04-12"`)) || !bytes.Contains(updateBody, []byte(`"date_end":"2026-04-12"`)) || !bytes.Contains(updateBody, []byte(`"date_start":"2026-04-13"`)) || !bytes.Contains(updateBody, []byte(`"date_end":"2026-04-13"`)) {
|
|
t.Fatalf("expected operational shift dates in update response, body=%s", string(updateBody))
|
|
}
|
|
|
|
resp, fixedBody := doJSONRequestBase(t, app, http.MethodPatch, "/api/v1/bases/update/"+idStr+"?category_type=hems", []byte(`{"data":{"type":"hems_base","id":"`+idStr+`","attributes":{"base":"With Ops Fixed","latitude":47.3769,"longitude":8.5417,"operational_shift_times":[{"date_start":"2026-06-03","date_end":"2026-06-04","start_time_type":"FIXED","end_time_type":"FIXED","shift_time":{"start":"03:00","end":"05:00"}}]}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(fixedBody, []byte(`"shift_time":{"start":"03:00","end":"05:00"}`)) {
|
|
t.Fatalf("expected fixed operational shift_time values in update response, body=%s", string(fixedBody))
|
|
}
|
|
|
|
resp, _ = doJSONRequestBase(t, app, http.MethodPatch, "/api/v1/bases/update/"+idStr+"?category_type=hems", []byte(`{"data":{"type":"hems_base","id":"`+idStr+`","attributes":{"latitude":47.3769,"longitude":8.5417,"operational_shift_times":[]}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastUpdateBase == nil || repo.lastUpdateBase.OperationalShiftTimes == nil || len(repo.lastUpdateBase.OperationalShiftTimes) != 0 {
|
|
t.Fatalf("expected explicit empty operational_shift_times to clear rows")
|
|
}
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodDelete, "/api/v1/bases/delete/"+idStr+"?category_type=hems", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastDeleteCategory != base.CategoryKeyHEMS {
|
|
t.Fatalf("expected hems category delete")
|
|
}
|
|
if !strings.Contains(string(body), "hems_base_delete") {
|
|
t.Fatalf("expected hems delete resource type")
|
|
}
|
|
|
|
resp, body = doJSONRequestBase(t, app, http.MethodDelete, "/api/v1/bases/delete/"+mustUUIDStringBase(t, uuidv7.MustBytes())+"?category_type=hems", nil)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for missing base delete, got %d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
}
|
|
|
|
func TestBaseHandlerUpdateShiftTimeOnly(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[base.CategoryKeyRegular][string(id)] = &base.Base{
|
|
ID: id,
|
|
BaseName: "Old",
|
|
DefaultShiftStart: "06:00:00",
|
|
DefaultShiftEnd: "21:00:00",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
idStr := mustUUIDStringBase(t, id)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodPatch, "/api/v1/bases/update/"+idStr, []byte(`{"data":{"type":"base","id":"`+idStr+`","attributes":{"shift_time":{"start":"07:00","end":"20:00"}}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastUpdateBase == nil || repo.lastUpdateBase.DefaultShiftStart == "" || repo.lastUpdateBase.DefaultShiftEnd == "" {
|
|
t.Fatalf("expected shift_time applied to repo update row")
|
|
}
|
|
if got := repo.lastUpdateBase.DefaultShiftStart; got != "07:00:00" {
|
|
t.Fatalf("expected updated start 07:00:00, got %s", got)
|
|
}
|
|
if got := repo.lastUpdateBase.DefaultShiftEnd; got != "20:00:00" {
|
|
t.Fatalf("expected updated end 20:00:00, got %s", got)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"shift_time":{"start":"07:00","end":"20:00"}`)) {
|
|
t.Fatalf("expected response shift_time to update, body=%s", string(body))
|
|
}
|
|
}
|
|
|
|
func TestBaseHandlerDelete_NotFoundAndConflict(t *testing.T) {
|
|
t.Run("not found returns 404", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodDelete, "/api/v1/bases/delete/"+mustUUIDStringBase(t, uuidv7.MustBytes())+"?category_type=hems", nil)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"BASE_NOT_FOUND"`)) {
|
|
t.Fatalf("expected base not found code, body=%s", string(body))
|
|
}
|
|
})
|
|
|
|
t.Run("relation conflict returns 409", func(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[base.CategoryKeyHEMS][string(id)] = &base.Base{ID: id, BaseName: "Has refs", DefaultShiftStart: "06:00:00", DefaultShiftEnd: "21:00:00", CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
|
repo.deleteBaseErr = apperrors.NewDeleteConflictError("takeover")
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodDelete, "/api/v1/bases/delete/"+mustUUIDStringBase(t, id)+"?category_type=hems", nil)
|
|
if resp.StatusCode != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"code":"BASE_DELETE_CONFLICT"`)) {
|
|
t.Fatalf("expected specific base delete conflict code, body=%s", string(body))
|
|
}
|
|
if !bytes.Contains(body, []byte(`"error_code":"4091615"`)) {
|
|
t.Fatalf("expected specific base delete conflict error_code, body=%s", string(body))
|
|
}
|
|
if !bytes.Contains(body, []byte(`takeover`)) {
|
|
t.Fatalf("expected relation detail to mention takeover, body=%s", string(body))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBaseHandlerListBases(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
repo.listRows = []base.Base{{
|
|
ID: uuidv7.MustBytes(),
|
|
BaseName: "Lude",
|
|
DefaultShiftStart: "06:00:00",
|
|
DefaultShiftEnd: "21:00:00",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}}
|
|
repo.listTotal = 1
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get-all?category_type=hems&search=abc&page=0&limit=101&sort=-base", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastListCategory != base.CategoryKeyHEMS || repo.lastListFilter != "abc" || repo.lastListSort != "base DESC" || repo.lastListLimit != 0 || repo.lastListOffset != 0 {
|
|
t.Fatalf("unexpected forwarded list args")
|
|
}
|
|
resp, _ = doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get-all?category_type=hems&sort=-sortkey", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastListSort != "is_active DESC, CASE WHEN is_active = 1 AND sortkey IS NOT NULL AND sortkey >= 0 THEN 0 ELSE 1 END ASC, CASE WHEN is_active = 1 AND sortkey IS NOT NULL AND sortkey >= 0 THEN sortkey END DESC, base ASC" {
|
|
t.Fatalf("unexpected sortkey sort forwarding: %q", repo.lastListSort)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"type":"hems_base"`)) {
|
|
t.Fatalf("expected hems_base list resource type")
|
|
}
|
|
if !bytes.Contains(body, []byte(`"base_category":"hems"`)) {
|
|
t.Fatalf("expected base_category hems")
|
|
}
|
|
if !bytes.Contains(body, []byte(`"shift_time":{"start":"06:00","end":"21:00"}`)) {
|
|
t.Fatalf("expected shift_time start/end in HH:MM format, body=%s", string(body))
|
|
}
|
|
if bytes.Contains(body, []byte(`"default_shift_start"`)) || bytes.Contains(body, []byte(`"default_shift_end"`)) || bytes.Contains(body, []byte(`"default_shift_time"`)) {
|
|
t.Fatalf("expected old default_shift_* fields removed, body=%s", string(body))
|
|
}
|
|
|
|
resp, _ = doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get-all?base_category=regular", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for base_category alias, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastListCategory != base.CategoryKeyRegular {
|
|
t.Fatalf("expected regular category from base_category alias, got %q", repo.lastListCategory)
|
|
}
|
|
|
|
resp, _ = doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get-all?base_category=hems", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 for base_category hems alias, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastListCategory != base.CategoryKeyHEMS {
|
|
t.Fatalf("expected hems category from base_category alias, got %q", repo.lastListCategory)
|
|
}
|
|
}
|
|
|
|
func TestBaseHandlerBaseResource_ShiftLabelsForNonFixedTypes(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[base.CategoryKeyRegular][string(id)] = &base.Base{
|
|
ID: id,
|
|
BaseName: "Twilight Base",
|
|
DefaultStartTimeType: base.ShiftTimeTypeBMCT,
|
|
DefaultEndTimeType: base.ShiftTimeTypeECET,
|
|
OperationalShiftTimes: []base.BaseOperationalShiftTime{
|
|
{
|
|
StartTimeType: base.ShiftTimeTypeBMCT,
|
|
EndTimeType: base.ShiftTimeTypeECET,
|
|
},
|
|
},
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, body := doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get/"+mustUUIDStringBase(t, id)+"?category_type=regular", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(body, []byte(`"shift_time":{"start":"BMCT","end":"ECET"}`)) {
|
|
t.Fatalf("expected BMCT/ECET shift labels in response, body=%s", string(body))
|
|
}
|
|
if !bytes.Contains(body, []byte(`"operational_shift_times":[{"date_start":"","date_end":"","start_time_type":"BMCT","end_time_type":"ECET","shift_time":{"start":"BMCT","end":"ECET"}}]`)) {
|
|
t.Fatalf("expected operational shift labels in response, body=%s", string(body))
|
|
}
|
|
}
|
|
|
|
func TestBaseHandlerListBasesDatatable(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
repo.listRows = []base.Base{{ID: uuidv7.MustBytes(), BaseName: "Reg", CreatedAt: time.Now(), UpdatedAt: time.Now()}}
|
|
repo.listTotal = 1
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, _ := doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get-all/dt?page=0&limit=0&size=101&draw=7&search=abc", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastListCategory != "" || repo.lastListSort != "" || repo.lastListLimit != 100 || repo.lastListOffset != 0 {
|
|
t.Fatalf("unexpected datatable args")
|
|
}
|
|
}
|
|
|
|
func TestBaseHandlerListAndCreateError(t *testing.T) {
|
|
repo := newMemBaseRepo()
|
|
repo.listBasesErr = errors.New("list failed")
|
|
repo.createBaseErr = errors.New("create failed")
|
|
h := NewBaseHandler(service.NewBaseService(repo))
|
|
app := newBaseTestApp(h)
|
|
|
|
resp, _ := doJSONRequestBase(t, app, http.MethodGet, "/api/v1/bases/get-all", nil)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 list, got %d", resp.StatusCode)
|
|
}
|
|
|
|
resp, _ = doJSONRequestBase(t, app, http.MethodPost, "/api/v1/bases/create", []byte(`{"data":{"type":"base","attributes":{"base":"A","latitude":47.3769,"longitude":8.5417,"default_shift_start":"`+testBaseShiftStart+`","default_shift_end":"`+testBaseShiftEnd+`"}}}`))
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 create, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestBaseEventsStream(t *testing.T) {
|
|
hub := filemanagerrealtime.NewHub(1, nil)
|
|
actor := uuidv7.MustBytes()
|
|
userUUID, _ := uuidv7.BytesToString(actor)
|
|
|
|
app := fiber.New()
|
|
fctx := &fasthttp.RequestCtx{}
|
|
fctx.Request.SetRequestURI("/api/v1/bases/events")
|
|
c := app.AcquireCtx(fctx)
|
|
c.Locals("user_id", actor)
|
|
if err := NewFileManagerEventsHandler(hub).Stream(c); err != nil {
|
|
t.Fatalf("unexpected stream setup error: %v", err)
|
|
}
|
|
|
|
hub.PublishToUsers([]string{userUUID}, filemanagerrealtime.FileStatusEvent{
|
|
FileUUID: mustUUIDStringBase(t, uuidv7.MustBytes()),
|
|
Status: "ready",
|
|
Name: "base.png",
|
|
MimeType: "image/png",
|
|
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
go func() {
|
|
time.Sleep(10 * time.Millisecond)
|
|
closeFirstHubSubscriberForUser(hub, userUUID)
|
|
}()
|
|
|
|
bodyStr := c.Context().Response.String()
|
|
app.ReleaseCtx(c)
|
|
if !strings.Contains(bodyStr, ": connected") {
|
|
t.Fatalf("expected connected comment in body: %q", bodyStr)
|
|
}
|
|
if !strings.Contains(bodyStr, "event: file.updated") {
|
|
t.Fatalf("expected file.updated event in body: %q", bodyStr)
|
|
}
|
|
if !strings.Contains(bodyStr, "\"file_id\"") || !strings.Contains(bodyStr, "\"status\":\"ready\"") {
|
|
t.Fatalf("expected file.updated payload in body: %q", bodyStr)
|
|
}
|
|
}
|