package handlers import ( "bytes" "context" "errors" "io" "net/http" "testing" "time" "github.com/gofiber/fiber/v2" "wucher/internal/domain/facility" "wucher/internal/service" "wucher/internal/shared/pkg/uuidv7" ) type memFacilityRepo struct { rows map[string]*facility.Facility createErr error updateErr error deleteErr error getByIDErr error listErr error listRows []facility.Facility listTotal int64 getByIDFn func(id []byte, call int) (*facility.Facility, error) getCalls int lastCreate *facility.Facility lastUpdate *facility.Facility lastDeleteID []byte lastListFilter string lastListCategory string lastListSort string lastListLimit int lastListOffset int } func newMemFacilityRepo() *memFacilityRepo { return &memFacilityRepo{rows: map[string]*facility.Facility{}} } func (r *memFacilityRepo) Create(_ context.Context, row *facility.Facility) error { if r.createErr != nil { return r.createErr } if len(row.ID) == 0 { row.ID = uuidv7.MustBytes() } r.rows[string(row.ID)] = row r.lastCreate = row return nil } func (r *memFacilityRepo) Update(_ context.Context, row *facility.Facility) error { if r.updateErr != nil { return r.updateErr } r.rows[string(row.ID)] = row r.lastUpdate = row return nil } func (r *memFacilityRepo) Delete(_ context.Context, id []byte, _ []byte) error { if r.deleteErr != nil { return r.deleteErr } r.lastDeleteID = append([]byte(nil), id...) delete(r.rows, string(id)) return nil } func (r *memFacilityRepo) GetByID(_ context.Context, id []byte) (*facility.Facility, error) { r.getCalls++ if r.getByIDFn != nil { return r.getByIDFn(id, r.getCalls) } if r.getByIDErr != nil { return nil, r.getByIDErr } return r.rows[string(id)], nil } func (r *memFacilityRepo) List(_ context.Context, filter, category, sort string, limit, offset int) ([]facility.Facility, int64, error) { r.lastListFilter = filter r.lastListCategory = category r.lastListSort = sort r.lastListLimit = limit r.lastListOffset = offset if r.listErr != nil { return nil, 0, r.listErr } return r.listRows, r.listTotal, nil } func newFacilityTestApp(h *FacilityHandler) *fiber.App { app := fiber.New() app.Post("/api/v1/facilities/create", h.Create) app.Patch("/api/v1/facilities/update/:uuid", h.Update) app.Delete("/api/v1/facilities/delete/:uuid", h.Delete) app.Get("/api/v1/facilities/get/:uuid", h.Get) app.Get("/api/v1/facilities/get-all", h.List) app.Get("/api/v1/facilities/get-all/dt", h.ListDatatable) return app } func doJSONRequestFacility(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 mustUUIDStringFacility(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 TestNewFacilityHandler(t *testing.T) { h := NewFacilityHandler(service.NewFacilityService(newMemFacilityRepo())) if h == nil || h.validate == nil { t.Fatalf("expected handler and validator initialized") } } func TestFacilityHandlerCreate(t *testing.T) { t.Run("invalid json", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodPost, "/api/v1/facilities/create", []byte(`{"data":`)) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("validation error", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodPost, "/api/v1/facilities/create", []byte(`{}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("required attrs cannot be empty after trim", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) body := []byte(`{"data":{"type":"facility","attributes":{"category":" ","name":"A","type":"B"}}}`) resp, _ := doJSONRequestFacility(t, app, http.MethodPost, "/api/v1/facilities/create", body) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("create error", func(t *testing.T) { repo := newMemFacilityRepo() repo.createErr = errors.New("create failed") h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) body := []byte(`{"data":{"type":"facility","attributes":{"category":"HEMS","name":"HEC Sling","type":"Sling"}}}`) resp, _ := doJSONRequestFacility(t, app, http.MethodPost, "/api/v1/facilities/create", body) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("success with refetch", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) actor := uuidv7.MustBytes() app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_id", actor) return c.Next() }) app.Post("/api/v1/facilities/create", h.Create) body := []byte(`{"data":{"type":"facility","attributes":{"category":" HEMS ","name":" HEC Sling ","type":" Sling ","sortkey":3,"length":" 10m ","weight":" 20kg ","electric":" yes ","note":" note ","is_active":false}}}`) resp, _ := doJSONRequestFacility(t, app, http.MethodPost, "/api/v1/facilities/create", body) if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } if repo.lastCreate == nil { t.Fatalf("expected row created") } if repo.lastCreate.Category != "HEMS" || repo.lastCreate.Name != "HEC Sling" || repo.lastCreate.Type != "Sling" { t.Fatalf("expected trimmed required fields") } if repo.lastCreate.SortKey == nil || *repo.lastCreate.SortKey != 3 { t.Fatalf("expected sortkey mapped on create") } if repo.lastCreate.Length != "10m" || repo.lastCreate.Weight != "20kg" || repo.lastCreate.Electric != "yes" || repo.lastCreate.Note != "note" || repo.lastCreate.IsActive { t.Fatalf("expected optional fields mapped and trimmed") } if string(repo.lastCreate.CreatedBy) != string(actor) || string(repo.lastCreate.UpdatedBy) != string(actor) { t.Fatalf("expected created_by and updated_by from actor") } }) t.Run("success fallback when refetch fails and defaults", func(t *testing.T) { repo := newMemFacilityRepo() repo.getByIDFn = func(_ []byte, call int) (*facility.Facility, error) { if call > 0 { return nil, errors.New("lookup failed") } return nil, nil } h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) body := []byte(`{"data":{"type":"facility","attributes":{"category":"DRY_LEASE","name":"Fuel Truck","type":"Ground"}}}`) resp, _ := doJSONRequestFacility(t, app, http.MethodPost, "/api/v1/facilities/create", body) if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } if repo.lastCreate == nil { t.Fatalf("expected row created") } if repo.lastCreate.Length != "" || repo.lastCreate.Weight != "" || repo.lastCreate.Electric != "" || repo.lastCreate.Note != "" || !repo.lastCreate.IsActive { t.Fatalf("expected default optional values") } }) } func TestFacilityHandlerUpdate(t *testing.T) { t.Run("invalid uuid", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/not-uuid", []byte(`{}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("invalid json", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+id, []byte(`{"data":`)) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("validation error", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+id, []byte(`{"data":{"type":"x","id":"`+id+`","attributes":{"name":"A"}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("id required", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+id, []byte(`{"data":{"type":"facility","id":"","attributes":{"name":"A"}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("id mismatch", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) other := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+id, []byte(`{"data":{"type":"facility","id":"`+other+`","attributes":{"name":"A"}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("no attributes", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+id, []byte(`{"data":{"type":"facility","id":"`+id+`","attributes":{}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("not found", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+id, []byte(`{"data":{"type":"facility","id":"`+id+`","attributes":{"name":"A"}}}`)) if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } }) t.Run("category empty", func(t *testing.T) { repo := newMemFacilityRepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &facility.Facility{ID: id, Category: "HEMS", Name: "Old", Type: "Sling"} h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) idStr := mustUUIDStringFacility(t, id) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+idStr, []byte(`{"data":{"type":"facility","id":"`+idStr+`","attributes":{"category":" "}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("name empty", func(t *testing.T) { repo := newMemFacilityRepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &facility.Facility{ID: id, Category: "HEMS", Name: "Old", Type: "Sling"} h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) idStr := mustUUIDStringFacility(t, id) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+idStr, []byte(`{"data":{"type":"facility","id":"`+idStr+`","attributes":{"name":" "}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("type empty allowed", func(t *testing.T) { repo := newMemFacilityRepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &facility.Facility{ID: id, Category: "HEMS", Name: "Old", Type: "Sling"} h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) idStr := mustUUIDStringFacility(t, id) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+idStr, []byte(`{"data":{"type":"facility","id":"`+idStr+`","attributes":{"type":" "}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil || repo.lastUpdate.Type != "" { t.Fatalf("expected type updated to empty string") } }) t.Run("update error", func(t *testing.T) { repo := newMemFacilityRepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &facility.Facility{ID: id, Category: "HEMS", Name: "Old", Type: "Sling"} repo.updateErr = errors.New("update failed") h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) idStr := mustUUIDStringFacility(t, id) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+idStr, []byte(`{"data":{"type":"facility","id":"`+idStr+`","attributes":{"name":"New"}}}`)) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("success with refetch", func(t *testing.T) { repo := newMemFacilityRepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &facility.Facility{ID: id, Category: "HEMS", Name: "Old", Type: "Sling", Length: "1m", Weight: "1kg", Electric: "", Note: "old", IsActive: true} h := NewFacilityHandler(service.NewFacilityService(repo)) actor := uuidv7.MustBytes() app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_id", actor) return c.Next() }) app.Patch("/api/v1/facilities/update/:uuid", h.Update) idStr := mustUUIDStringFacility(t, id) body := []byte(`{"data":{"type":"facility","id":"` + idStr + `","attributes":{"category":" DRY_LEASE ","name":" New Name ","type":" Truck ","sortkey":7,"length":" 12m ","weight":" 50kg ","electric":" AC ","note":" updated ","is_active":false}}}`) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+idStr, body) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil { t.Fatalf("expected row updated") } if repo.lastUpdate.Category != "DRY_LEASE" || repo.lastUpdate.Name != "New Name" || repo.lastUpdate.Type != "Truck" { t.Fatalf("expected trimmed required fields") } if repo.lastUpdate.SortKey == nil || *repo.lastUpdate.SortKey != 7 { t.Fatalf("expected sortkey mapped on update") } if repo.lastUpdate.Length != "12m" || repo.lastUpdate.Weight != "50kg" || repo.lastUpdate.Electric != "AC" || repo.lastUpdate.Note != "updated" || repo.lastUpdate.IsActive { t.Fatalf("expected updated optional values") } if string(repo.lastUpdate.UpdatedBy) != string(actor) { t.Fatalf("expected updated_by from actor") } }) t.Run("success fallback when refetch fails", func(t *testing.T) { repo := newMemFacilityRepo() id := uuidv7.MustBytes() existing := &facility.Facility{ID: id, Category: "HEMS", Name: "Old", Type: "Sling"} repo.getByIDFn = func(_ []byte, call int) (*facility.Facility, error) { if call == 1 { return existing, nil } return nil, errors.New("second lookup failed") } h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) idStr := mustUUIDStringFacility(t, id) resp, _ := doJSONRequestFacility(t, app, http.MethodPatch, "/api/v1/facilities/update/"+idStr, []byte(`{"data":{"type":"facility","id":"`+idStr+`","attributes":{"note":" only note "}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil || repo.lastUpdate.Note != "only note" { t.Fatalf("expected note updated in fallback path") } }) } func TestFacilityHandlerDelete(t *testing.T) { t.Run("invalid uuid", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodDelete, "/api/v1/facilities/delete/not-uuid", nil) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("delete error", func(t *testing.T) { repo := newMemFacilityRepo() repo.deleteErr = errors.New("delete failed") h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodDelete, "/api/v1/facilities/delete/"+id, nil) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("success", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := uuidv7.MustBytes() idStr := mustUUIDStringFacility(t, id) resp, _ := doJSONRequestFacility(t, app, http.MethodDelete, "/api/v1/facilities/delete/"+idStr, nil) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if string(repo.lastDeleteID) != string(id) { t.Fatalf("expected deleted id captured") } }) } func TestFacilityHandlerGet(t *testing.T) { t.Run("invalid uuid", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get/not-uuid", nil) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("not found", func(t *testing.T) { repo := newMemFacilityRepo() h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) id := mustUUIDStringFacility(t, uuidv7.MustBytes()) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get/"+id, nil) if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } }) t.Run("success", func(t *testing.T) { repo := newMemFacilityRepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &facility.Facility{ID: id, Category: "HEMS", Name: "HEC Sling", Type: "Sling", CreatedAt: time.Now(), UpdatedAt: time.Now()} h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) idStr := mustUUIDStringFacility(t, id) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get/"+idStr, nil) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } }) } func TestFacilityHandlerList(t *testing.T) { t.Run("list error", func(t *testing.T) { repo := newMemFacilityRepo() repo.listErr = errors.New("list failed") h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get-all", nil) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("success with category trim and clamp max", func(t *testing.T) { repo := newMemFacilityRepo() repo.listRows = []facility.Facility{{ID: uuidv7.MustBytes(), Category: "HEMS", Name: "HEC Sling", Type: "Sling", CreatedAt: time.Now(), UpdatedAt: time.Now()}} repo.listTotal = 1 h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get-all?search=abc&category=%20HEMS%20&page=0&limit=101&sort=-sortkey", nil) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastListFilter != "abc" || repo.lastListCategory != "HEMS" || 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, name ASC" || repo.lastListLimit != 0 || repo.lastListOffset != 0 { t.Fatalf("unexpected list params: filter=%q category=%q sort=%q limit=%d offset=%d", repo.lastListFilter, repo.lastListCategory, repo.lastListSort, repo.lastListLimit, repo.lastListOffset) } }) t.Run("success with clamp min", func(t *testing.T) { repo := newMemFacilityRepo() repo.listRows = []facility.Facility{} repo.listTotal = 0 h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get-all?page=2&limit=0&size=-1&search=needle", nil) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastListFilter != "needle" || repo.lastListCategory != "" || repo.lastListLimit != 0 || repo.lastListOffset != 0 { t.Fatalf("unexpected list params: filter=%q category=%q limit=%d offset=%d", repo.lastListFilter, repo.lastListCategory, repo.lastListLimit, repo.lastListOffset) } }) } func TestFacilityHandlerListDatatable(t *testing.T) { t.Run("list error", func(t *testing.T) { repo := newMemFacilityRepo() repo.listErr = errors.New("list failed") h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get-all/dt", nil) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("success clamp max and page floor", func(t *testing.T) { repo := newMemFacilityRepo() repo.listRows = []facility.Facility{{ID: uuidv7.MustBytes(), Category: "DRY_LEASE", Name: "Fuel Truck", Type: "Ground", CreatedAt: time.Now(), UpdatedAt: time.Now()}} repo.listTotal = 1 h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get-all/dt?page=0&limit=0&size=101&draw=7&search=abc&category=%20DRY_LEASE%20", nil) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastListFilter != "abc" || repo.lastListCategory != "DRY_LEASE" || repo.lastListSort != "" || repo.lastListLimit != 100 || repo.lastListOffset != 0 { t.Fatalf("unexpected list params: filter=%q category=%q sort=%q limit=%d offset=%d", repo.lastListFilter, repo.lastListCategory, repo.lastListSort, repo.lastListLimit, repo.lastListOffset) } }) t.Run("success clamp min", func(t *testing.T) { repo := newMemFacilityRepo() repo.listRows = []facility.Facility{} repo.listTotal = 0 h := NewFacilityHandler(service.NewFacilityService(repo)) app := newFacilityTestApp(h) resp, _ := doJSONRequestFacility(t, app, http.MethodGet, "/api/v1/facilities/get-all/dt?page=2&limit=0&size=-1", nil) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastListLimit != 20 || repo.lastListOffset != 20 { t.Fatalf("unexpected list params: limit=%d offset=%d", repo.lastListLimit, repo.lastListOffset) } }) } func TestFacilityResource(t *testing.T) { now := time.Now().UTC().Truncate(time.Second) row := &facility.Facility{ ID: uuidv7.MustBytes(), Category: "HEMS", Name: "HEC Sling", Type: "Sling", SortKey: intPtrFacilityHandler(5), Length: "10m", Weight: "20kg", Electric: "yes", Note: "ready", IsActive: true, CreatedBy: uuidv7.MustBytes(), UpdatedBy: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now, } res := facilityResource(row) if res.Type != "facility" { t.Fatalf("unexpected type: %s", res.Type) } if res.ID == "" { t.Fatalf("expected resource id") } if res.Attributes.Category != "HEMS" || res.Attributes.Name != "HEC Sling" || res.Attributes.Type != "Sling" { t.Fatalf("expected required attributes in resource") } if res.Attributes.Length != "10m" || res.Attributes.Weight != "20kg" || res.Attributes.Electric != "yes" || res.Attributes.Note != "ready" || !res.Attributes.IsActive { t.Fatalf("expected optional attributes in resource") } if res.Attributes.SortKey == nil || *res.Attributes.SortKey != 5 { t.Fatalf("expected sortkey in resource") } if res.Attributes.CreatedAt == "" || res.Attributes.UpdatedAt == "" { t.Fatalf("expected timestamp attributes in resource") } if res.Attributes.CreatedBy == "" { t.Fatalf("expected created_by in resource") } if res.Attributes.UpdatedBy == "" { t.Fatalf("expected updated_by in resource") } } func intPtrFacilityHandler(v int) *int { return &v }