747 lines
25 KiB
Go
747 lines
25 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
mysqldriver "github.com/go-sql-driver/mysql"
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
"wucher/internal/domain/land"
|
|
"wucher/internal/service"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
"wucher/internal/transport/http/dto"
|
|
)
|
|
|
|
type memLandRepo struct {
|
|
rows map[string]*land.Land
|
|
|
|
createErr error
|
|
updateErr error
|
|
deleteErr error
|
|
getByIDErr error
|
|
listErr error
|
|
|
|
listRows []land.Land
|
|
listTotal int64
|
|
|
|
getByIDFn func(id []byte, call int) (*land.Land, error)
|
|
getCalls int
|
|
|
|
lastCreate *land.Land
|
|
lastUpdate *land.Land
|
|
lastDeleteID []byte
|
|
lastDeleteBy []byte
|
|
lastListFilter string
|
|
lastListSort string
|
|
lastListLimit int
|
|
lastListOffset int
|
|
}
|
|
|
|
func newMemLandRepo() *memLandRepo {
|
|
return &memLandRepo{
|
|
rows: map[string]*land.Land{},
|
|
}
|
|
}
|
|
|
|
func (r *memLandRepo) Create(_ context.Context, row *land.Land) 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 *memLandRepo) Update(_ context.Context, row *land.Land) error {
|
|
if r.updateErr != nil {
|
|
return r.updateErr
|
|
}
|
|
r.rows[string(row.ID)] = row
|
|
r.lastUpdate = row
|
|
return nil
|
|
}
|
|
|
|
func (r *memLandRepo) Delete(_ context.Context, id []byte, deletedBy []byte) error {
|
|
if r.deleteErr != nil {
|
|
return r.deleteErr
|
|
}
|
|
r.lastDeleteID = append([]byte(nil), id...)
|
|
r.lastDeleteBy = append([]byte(nil), deletedBy...)
|
|
delete(r.rows, string(id))
|
|
return nil
|
|
}
|
|
|
|
func (r *memLandRepo) GetByID(_ context.Context, id []byte) (*land.Land, 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 *memLandRepo) List(_ context.Context, filter, sort string, limit, offset int) ([]land.Land, int64, error) {
|
|
r.lastListFilter = filter
|
|
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 newLandTestApp(h *LandHandler, actor []byte) *fiber.App {
|
|
app := fiber.New()
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
if len(actor) > 0 {
|
|
c.Locals("user_id", actor)
|
|
}
|
|
return c.Next()
|
|
})
|
|
app.Post("/api/v1/land/create", h.Create)
|
|
app.Patch("/api/v1/land/update/:id", h.Update)
|
|
app.Delete("/api/v1/land/delete/:id", h.Delete)
|
|
app.Get("/api/v1/land/get-all", h.List)
|
|
app.Get("/api/v1/land/get-all/dt", h.ListDatatable)
|
|
return app
|
|
}
|
|
|
|
func doJSONRequestLand(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 mustUUIDStringLand(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 TestNewLandHandler(t *testing.T) {
|
|
h := NewLandHandler(service.NewLandService(newMemLandRepo()))
|
|
if h == nil || h.validate == nil {
|
|
t.Fatalf("expected handler and validator initialized")
|
|
}
|
|
}
|
|
|
|
type landViewServiceMock struct {
|
|
land.Service
|
|
getByIDViewFn func(context.Context, []byte) (*dto.LandView, error)
|
|
listViewFn func(context.Context, string, string, int, int) ([]dto.LandView, int64, error)
|
|
}
|
|
|
|
func (m *landViewServiceMock) GetByIDView(ctx context.Context, id []byte) (*dto.LandView, error) {
|
|
return m.getByIDViewFn(ctx, id)
|
|
}
|
|
|
|
func (m *landViewServiceMock) ListView(ctx context.Context, filter, sort string, limit, offset int) ([]dto.LandView, int64, error) {
|
|
return m.listViewFn(ctx, filter, sort, limit, offset)
|
|
}
|
|
|
|
func TestLandHandlerViewHelpers(t *testing.T) {
|
|
t.Run("getByIDView via extended interface", func(t *testing.T) {
|
|
id := uuidv7.MustBytes()
|
|
want := &dto.LandView{Row: land.Land{ID: id, Name: "Austria"}}
|
|
h := NewLandHandler(&landViewServiceMock{
|
|
getByIDViewFn: func(_ context.Context, gotID []byte) (*dto.LandView, error) {
|
|
if string(gotID) != string(id) {
|
|
t.Fatalf("unexpected id")
|
|
}
|
|
return want, nil
|
|
},
|
|
})
|
|
got, err := h.getByIDView(context.Background(), id)
|
|
if err != nil || got == nil || got.Row.Name != "Austria" {
|
|
t.Fatalf("expected extended getByIDView path")
|
|
}
|
|
})
|
|
|
|
t.Run("getByIDView fallback via base service", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &land.Land{ID: id, Name: "Germany"}
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
got, err := h.getByIDView(context.Background(), id)
|
|
if err != nil || got == nil || got.Row.Name != "Germany" {
|
|
t.Fatalf("expected fallback getByIDView")
|
|
}
|
|
})
|
|
|
|
t.Run("listView via extended interface", func(t *testing.T) {
|
|
h := NewLandHandler(&landViewServiceMock{
|
|
listViewFn: func(_ context.Context, filter, sort string, limit, offset int) ([]dto.LandView, int64, error) {
|
|
if filter != "eu" || sort != "name ASC" || limit != 10 || offset != 5 {
|
|
t.Fatalf("unexpected list args")
|
|
}
|
|
return []dto.LandView{{Row: land.Land{Name: "Italy"}}}, 1, nil
|
|
},
|
|
})
|
|
rows, total, err := h.listView(context.Background(), "eu", "name ASC", 10, 5)
|
|
if err != nil || total != 1 || len(rows) != 1 || rows[0].Row.Name != "Italy" {
|
|
t.Fatalf("expected extended listView")
|
|
}
|
|
})
|
|
|
|
t.Run("listView fallback via base service", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.listRows = []land.Land{{ID: uuidv7.MustBytes(), Name: "France"}}
|
|
repo.listTotal = 1
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
rows, total, err := h.listView(context.Background(), "", "name ASC", 20, 0)
|
|
if err != nil || total != 1 || len(rows) != 1 || rows[0].Row.Name != "France" {
|
|
t.Fatalf("expected fallback listView")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLandHandlerCreate(t *testing.T) {
|
|
t.Run("invalid json", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPost, "/api/v1/land/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 := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPost, "/api/v1/land/create", []byte(`{}`))
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("name empty", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
body := []byte(`{"data":{"type":"land","attributes":{"name":" "}}}`)
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPost, "/api/v1/land/create", body)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("create error", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.createErr = errors.New("create failed")
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
body := []byte(`{"data":{"type":"land","attributes":{"name":"Germany"}}}`)
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPost, "/api/v1/land/create", body)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate iso_code", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.createErr = &mysqldriver.MySQLError{
|
|
Number: 1062,
|
|
Message: "Duplicate entry 'DE' for key 'uq_lands_land_iso_code'",
|
|
}
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
body := []byte(`{"data":{"type":"land","attributes":{"name":"Germany","iso_code":"DE"}}}`)
|
|
resp, respBody := doJSONRequestLand(t, app, http.MethodPost, "/api/v1/land/create", body)
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(respBody, []byte(`"iso_code already exists"`)) {
|
|
t.Fatalf("expected duplicate iso_code detail, got %s", string(respBody))
|
|
}
|
|
})
|
|
|
|
t.Run("success with refetch", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
actor := uuidv7.MustBytes()
|
|
app := newLandTestApp(h, actor)
|
|
|
|
body := []byte(`{"data":{"type":"land","attributes":{"name":"Germany","note":" EU region ","iso_code":"de","bmd_export_id":" DE001 ","sortkey":3,"is_active":false}}}`)
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPost, "/api/v1/land/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.Name != "Germany" {
|
|
t.Fatalf("expected attributes to be mapped and trimmed")
|
|
}
|
|
if repo.lastCreate.LandISOCode != "DE" {
|
|
t.Fatalf("expected iso_code normalized")
|
|
}
|
|
if repo.lastCreate.BMDExportID == nil || *repo.lastCreate.BMDExportID != "DE001" {
|
|
t.Fatalf("expected bmd_export_id normalized")
|
|
}
|
|
if repo.lastCreate.Note != "EU region" {
|
|
t.Fatalf("expected note normalized")
|
|
}
|
|
if repo.lastCreate.SortKey == nil || *repo.lastCreate.SortKey != 3 {
|
|
t.Fatalf("expected sortkey=3 to be mapped")
|
|
}
|
|
if repo.lastCreate.IsActive {
|
|
t.Fatalf("expected is_active=false to be mapped")
|
|
}
|
|
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 error", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.getByIDFn = func(_ []byte, call int) (*land.Land, error) {
|
|
if call > 0 {
|
|
return nil, errors.New("lookup failed")
|
|
}
|
|
return nil, nil
|
|
}
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
body := []byte(`{"data":{"type":"land","attributes":{"name":"Germany"}}}`)
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPost, "/api/v1/land/create", body)
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLandHandlerUpdate(t *testing.T) {
|
|
t.Run("invalid uuid", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/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 := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
id := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/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 := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
id := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/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 := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
id := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+id, []byte(`{"data":{"type":"land","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 := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
id := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
other := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+id, []byte(`{"data":{"type":"land","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 := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
id := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+id, []byte(`{"data":{"type":"land","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 := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
id := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+id, []byte(`{"data":{"type":"land","id":"`+id+`","attributes":{"name":"A"}}}`))
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("name empty", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &land.Land{ID: id, Name: "Old"}
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
idStr := mustUUIDStringLand(t, id)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+idStr, []byte(`{"data":{"type":"land","id":"`+idStr+`","attributes":{"name":" "}}}`))
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("update error", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &land.Land{ID: id, Name: "Old"}
|
|
repo.updateErr = errors.New("update failed")
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
idStr := mustUUIDStringLand(t, id)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+idStr, []byte(`{"data":{"type":"land","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 := newMemLandRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &land.Land{ID: id, Name: "Old"}
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
actor := uuidv7.MustBytes()
|
|
app := newLandTestApp(h, actor)
|
|
idStr := mustUUIDStringLand(t, id)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+idStr, []byte(`{"data":{"type":"land","id":"`+idStr+`","attributes":{"name":" New ","note":" updated note ","iso_code":"id","bmd_export_id":" ID123 ","sortkey":2,"is_active":false}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastUpdate == nil || repo.lastUpdate.Name != "New" {
|
|
t.Fatalf("expected updated values")
|
|
}
|
|
if repo.lastUpdate.LandISOCode != "ID" {
|
|
t.Fatalf("expected updated iso_code")
|
|
}
|
|
if repo.lastUpdate.BMDExportID == nil || *repo.lastUpdate.BMDExportID != "ID123" {
|
|
t.Fatalf("expected updated bmd_export_id")
|
|
}
|
|
if repo.lastUpdate.Note != "updated note" {
|
|
t.Fatalf("expected updated note")
|
|
}
|
|
if repo.lastUpdate.SortKey == nil || *repo.lastUpdate.SortKey != 2 {
|
|
t.Fatalf("expected updated sortkey=2")
|
|
}
|
|
if repo.lastUpdate.IsActive {
|
|
t.Fatalf("expected is_active=false updated")
|
|
}
|
|
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 := newMemLandRepo()
|
|
id := uuidv7.MustBytes()
|
|
existing := &land.Land{ID: id, Name: "Old"}
|
|
repo.getByIDFn = func(_ []byte, call int) (*land.Land, error) {
|
|
if call == 1 {
|
|
return existing, nil
|
|
}
|
|
return nil, errors.New("second lookup failed")
|
|
}
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
idStr := mustUUIDStringLand(t, id)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+idStr, []byte(`{"data":{"type":"land","id":"`+idStr+`","attributes":{"name":"Only"}}}`))
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate iso_code", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
id := uuidv7.MustBytes()
|
|
repo.rows[string(id)] = &land.Land{
|
|
ID: id,
|
|
Name: "Germany",
|
|
LandISOCode: "DE",
|
|
IsActive: true,
|
|
}
|
|
repo.updateErr = &mysqldriver.MySQLError{
|
|
Number: 1062,
|
|
Message: "Duplicate entry 'ID' for key 'uq_lands_land_iso_code'",
|
|
}
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
idStr := mustUUIDStringLand(t, id)
|
|
|
|
resp, respBody := doJSONRequestLand(t, app, http.MethodPatch, "/api/v1/land/update/"+idStr, []byte(`{"data":{"type":"land","id":"`+idStr+`","attributes":{"iso_code":"ID"}}}`))
|
|
if resp.StatusCode != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422, got %d", resp.StatusCode)
|
|
}
|
|
if !bytes.Contains(respBody, []byte(`"iso_code already exists"`)) {
|
|
t.Fatalf("expected duplicate iso_code detail, got %s", string(respBody))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLandHandlerDelete(t *testing.T) {
|
|
t.Run("invalid uuid", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodDelete, "/api/v1/land/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 := newMemLandRepo()
|
|
repo.deleteErr = errors.New("delete failed")
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
id := mustUUIDStringLand(t, uuidv7.MustBytes())
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodDelete, "/api/v1/land/delete/"+id, nil)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
actor := uuidv7.MustBytes()
|
|
app := newLandTestApp(h, actor)
|
|
id := uuidv7.MustBytes()
|
|
idStr := mustUUIDStringLand(t, id)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodDelete, "/api/v1/land/delete/"+idStr, nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if string(repo.lastDeleteBy) != string(actor) {
|
|
t.Fatalf("expected deleted_by from actor")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLandHandlerList(t *testing.T) {
|
|
t.Run("list error", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.listErr = errors.New("list failed")
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodGet, "/api/v1/land/get-all", nil)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("success with filter name and clamp max", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.listRows = []land.Land{{ID: uuidv7.MustBytes(), Name: "A", SortKey: intPtrLandHandler(3), CreatedAt: time.Now(), UpdatedAt: time.Now()}}
|
|
repo.listTotal = 1
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodGet, "/api/v1/land/get-all?filter[name]=abc&page[number]=0&page=0&page[size]=0&limit=101&sort=-sortkey", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastListFilter != "abc" || 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 sort=%q limit=%d offset=%d", repo.lastListFilter, repo.lastListSort, repo.lastListLimit, repo.lastListOffset)
|
|
}
|
|
})
|
|
|
|
t.Run("success with search and clamp min", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.listRows = []land.Land{}
|
|
repo.listTotal = 0
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodGet, "/api/v1/land/get-all?page=2&size=-1&search=needle", nil)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
if repo.lastListFilter != "needle" || repo.lastListLimit != 0 || repo.lastListOffset != 0 {
|
|
t.Fatalf("unexpected list params: filter=%q limit=%d offset=%d", repo.lastListFilter, repo.lastListLimit, repo.lastListOffset)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLandHandlerListDatatable(t *testing.T) {
|
|
t.Run("list error", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.listErr = errors.New("list failed")
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodGet, "/api/v1/land/get-all/dt", nil)
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("success page floor without clamping small custom size", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.listRows = []land.Land{{ID: uuidv7.MustBytes(), Name: "A", CreatedAt: time.Now(), UpdatedAt: time.Now()}}
|
|
repo.listTotal = 1
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodGet, "/api/v1/land/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.lastListFilter != "abc" || repo.lastListLimit != 101 || repo.lastListOffset != 0 {
|
|
t.Fatalf("unexpected list params: filter=%q limit=%d offset=%d", repo.lastListFilter, repo.lastListLimit, repo.lastListOffset)
|
|
}
|
|
})
|
|
|
|
t.Run("success clamp min", func(t *testing.T) {
|
|
repo := newMemLandRepo()
|
|
repo.listRows = []land.Land{}
|
|
repo.listTotal = 0
|
|
h := NewLandHandler(service.NewLandService(repo))
|
|
app := newLandTestApp(h, nil)
|
|
|
|
resp, _ := doJSONRequestLand(t, app, http.MethodGet, "/api/v1/land/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 TestLandResource(t *testing.T) {
|
|
id := uuidv7.MustBytes()
|
|
createdBy := uuidv7.MustBytes()
|
|
updatedBy := uuidv7.MustBytes()
|
|
deletedBy := uuidv7.MustBytes()
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
deletedAt := now.Add(time.Hour)
|
|
|
|
row := &dto.LandView{
|
|
Row: land.Land{
|
|
ID: id,
|
|
Name: "Germany",
|
|
Note: "EU region",
|
|
LandISOCode: "DE",
|
|
BMDExportID: strPtrLandHandler("DE001"),
|
|
SortKey: intPtrLandHandler(2),
|
|
IsActive: true,
|
|
CreatedAt: now,
|
|
CreatedBy: createdBy,
|
|
UpdatedAt: now,
|
|
UpdatedBy: updatedBy,
|
|
DeletedAt: &deletedAt,
|
|
DeletedBy: deletedBy,
|
|
},
|
|
FederalStateTotal: 2,
|
|
FederalStateList: []dto.LandFederalStateView{
|
|
{ID: uuidv7.MustBytes(), Name: "Bayern"},
|
|
{ID: uuidv7.MustBytes(), Name: "Berlin"},
|
|
},
|
|
}
|
|
|
|
res := landResource(row)
|
|
if res.Type != "land" {
|
|
t.Fatalf("unexpected type: %s", res.Type)
|
|
}
|
|
if res.ID == "" || res.Attributes.CreatedBy == "" || res.Attributes.UpdatedBy == "" || res.Attributes.DeletedBy == "" {
|
|
t.Fatalf("expected UUID fields to be mapped")
|
|
}
|
|
if res.Attributes.Name != "Germany" {
|
|
t.Fatalf("unexpected attribute mapping")
|
|
}
|
|
if res.Attributes.Note != "EU region" {
|
|
t.Fatalf("expected note mapped")
|
|
}
|
|
if res.Attributes.ISOCode != "DE" {
|
|
t.Fatalf("expected iso_code mapped")
|
|
}
|
|
if res.Attributes.BMDExportID == nil || *res.Attributes.BMDExportID != "DE001" {
|
|
t.Fatalf("expected bmd_export_id mapped")
|
|
}
|
|
if res.Attributes.FederalStateTotal != 2 || len(res.Attributes.FederalStateList) != 2 {
|
|
t.Fatalf("expected federal_state aggregate fields mapped")
|
|
}
|
|
if res.Attributes.SortKey == nil || *res.Attributes.SortKey != 2 {
|
|
t.Fatalf("expected sortkey mapped")
|
|
}
|
|
if !res.Attributes.IsActive {
|
|
t.Fatalf("expected is_active mapped")
|
|
}
|
|
if res.Attributes.CreatedAt == "" || res.Attributes.UpdatedAt == "" || res.Attributes.DeletedAt == "" {
|
|
t.Fatalf("expected time fields to be mapped")
|
|
}
|
|
}
|
|
|
|
func intPtrLandHandler(v int) *int { return &v }
|
|
|
|
func strPtrLandHandler(v string) *string { return &v }
|