package handlers import ( "bytes" "context" "errors" "io" "net/http" "strings" "testing" "time" "github.com/gofiber/fiber/v2" "wucher/internal/domain/federal_state" "wucher/internal/domain/icao" "wucher/internal/domain/land" "wucher/internal/service" "wucher/internal/shared/pkg/uuidv7" ) type memICAORepo struct { rows map[string]*icao.ICAO createErr error updateErr error deleteErr error getByIDErr error listErr error listRows []icao.ICAO listTotal int64 getByIDFn func(id []byte, call int) (*icao.ICAO, error) getCalls int lastCreate *icao.ICAO lastUpdate *icao.ICAO lastDeleteID []byte lastDeleteBy []byte lastListFilter string lastListSort string lastListLimit int lastListOffset int } func newMemICAORepo() *memICAORepo { return &memICAORepo{ rows: map[string]*icao.ICAO{}, } } func (r *memICAORepo) Create(_ context.Context, row *icao.ICAO) 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 *memICAORepo) Update(_ context.Context, row *icao.ICAO) error { if r.updateErr != nil { return r.updateErr } r.rows[string(row.ID)] = row r.lastUpdate = row return nil } func (r *memICAORepo) 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 *memICAORepo) GetByID(_ context.Context, id []byte) (*icao.ICAO, 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 *memICAORepo) List(_ context.Context, filter, sort string, limit, offset int) ([]icao.ICAO, 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 newICAOTestApp(h *ICAOHandler, 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/icao/create", h.Create) app.Patch("/api/v1/icao/update/:uuid", h.Update) app.Delete("/api/v1/icao/delete/:uuid", h.Delete) app.Get("/api/v1/icao/get-all", h.List) app.Get("/api/v1/icao/get-all/dt", h.ListDatatable) return app } func doJSONRequestICAO(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 mustUUIDStringICAO(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 TestNewICAOHandler(t *testing.T) { h := NewICAOHandler(service.NewICAOService(newMemICAORepo())) if h == nil || h.validate == nil { t.Fatalf("expected handler and validator initialized") } } func TestICAOHandlerCreate(t *testing.T) { t.Run("invalid json", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/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 := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/create", []byte(`{}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("name empty", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) body := []byte(`{"data":{"type":"icao","attributes":{"icao_code":" "}}}`) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/create", body) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("create error", func(t *testing.T) { repo := newMemICAORepo() repo.createErr = errors.New("create failed") h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) body := []byte(`{"data":{"type":"icao","attributes":{"icao_code":"EDDB"}}}`) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/create", body) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("invalid federal_state_id", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) body := []byte(`{"data":{"type":"icao","attributes":{"icao_code":"EDDB","federal_state_id":"not-uuid"}}}`) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/create", body) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("success with refetch", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) actor := uuidv7.MustBytes() app := newICAOTestApp(h, actor) body := []byte(`{"data":{"type":"icao","attributes":{"icao_code":"EDDB","sortkey":3,"address":"Main Street","landline_number":"021123456","mobile_number":"+491234","email":"main@hic.local","is_active":false}}}`) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/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.ICAOCode != "EDDB" || repo.lastCreate.Address != "Main Street" || repo.lastCreate.LandlineNumber != "021123456" || repo.lastCreate.MobileNumber != "+491234" || repo.lastCreate.Email != "main@hic.local" { t.Fatalf("expected optional attributes to be mapped") } if repo.lastCreate.SortKey == nil || *repo.lastCreate.SortKey != 3 { t.Fatalf("expected sortkey mapped") } if repo.lastCreate.IsActive { t.Fatalf("expected is_active false") } 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 with federal_state_id", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) fsID := mustUUIDStringICAO(t, uuidv7.MustBytes()) body := []byte(`{"data":{"type":"icao","attributes":{"icao_code":"EDDF","federal_state_id":"` + fsID + `","note":" test note "}}}`) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/create", body) if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } if repo.lastCreate == nil || len(repo.lastCreate.FederalStateID) == 0 { t.Fatalf("expected federal_state_id mapped") } if repo.lastCreate.Note != "test note" { t.Fatalf("expected note trimmed") } }) t.Run("success fallback when refetch error", func(t *testing.T) { repo := newMemICAORepo() repo.getByIDFn = func(_ []byte, call int) (*icao.ICAO, error) { if call > 0 { return nil, errors.New("lookup failed") } return nil, nil } h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) body := []byte(`{"data":{"type":"icao","attributes":{"icao_code":"EDDB"}}}`) resp, _ := doJSONRequestICAO(t, app, http.MethodPost, "/api/v1/icao/create", body) if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } }) } func TestICAOHandlerUpdate(t *testing.T) { t.Run("invalid uuid", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/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 := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) id := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/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 := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) id := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+id, []byte(`{"data":{"type":"x","id":"`+id+`","attributes":{"icao_code":"A"}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("id required", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) id := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+id, []byte(`{"data":{"type":"icao","id":"","attributes":{"icao_code":"A"}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("id mismatch", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) id := mustUUIDStringICAO(t, uuidv7.MustBytes()) other := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+id, []byte(`{"data":{"type":"icao","id":"`+other+`","attributes":{"icao_code":"A"}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("no attributes", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) id := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+id, []byte(`{"data":{"type":"icao","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 := newMemICAORepo() repo.getByIDErr = errors.New("lookup failed") h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) id := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+id, []byte(`{"data":{"type":"icao","id":"`+id+`","attributes":{"icao_code":"A"}}}`)) if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } }) t.Run("name empty", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ID: id, ICAOCode: "OLD"} h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"icao_code":" "}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("update error", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ID: id, ICAOCode: "OLD"} repo.updateErr = errors.New("update failed") h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"icao_code":"NEW"}}}`)) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("invalid federal_state_id", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ID: id, ICAOCode: "OLD"} h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"federal_state_id":"not-uuid"}}}`)) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } }) t.Run("clear federal_state_id by empty string", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ ID: id, ICAOCode: "OLD", FederalStateID: uuidv7.MustBytes(), } h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"federal_state_id":" "}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil { t.Fatalf("expected update called") } if repo.lastUpdate.FederalStateID != nil { t.Fatalf("expected federal_state_id to be cleared") } }) t.Run("success with refetch", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ID: id, ICAOCode: "OLD"} h := NewICAOHandler(service.NewICAOService(repo)) actor := uuidv7.MustBytes() app := newICAOTestApp(h, actor) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"icao_code":"NEW","address":"Street 1","landline_number":"021999","mobile_number":"+49888","email":"new@hic.local"}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil || repo.lastUpdate.ICAOCode != "NEW" || repo.lastUpdate.Address != "Street 1" || repo.lastUpdate.LandlineNumber != "021999" || repo.lastUpdate.MobileNumber != "+49888" || repo.lastUpdate.Email != "new@hic.local" { t.Fatalf("expected updated values") } if string(repo.lastUpdate.UpdatedBy) != string(actor) { t.Fatalf("expected updated_by from actor") } }) t.Run("success update sortkey and is_active", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ID: id, ICAOCode: "OLD", IsActive: true} h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"sortkey":2,"is_active":false}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil { t.Fatalf("expected update called") } if repo.lastUpdate.SortKey == nil || *repo.lastUpdate.SortKey != 2 { t.Fatalf("expected sortkey updated") } if repo.lastUpdate.IsActive { t.Fatalf("expected is_active false") } }) t.Run("success clear sortkey with null", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ID: id, ICAOCode: "OLD", SortKey: intPtrICAOHandler(7)} h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"sortkey":null}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil || repo.lastUpdate.SortKey != nil { t.Fatalf("expected sortkey cleared") } }) t.Run("success set federal_state_id", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() oldFSID := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ ID: id, ICAOCode: "OLD", FederalStateID: oldFSID, FederalState: &federal_state.FederalState{ID: oldFSID, Name: "Old FS"}, } h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) fsID := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"federal_state_id":"`+fsID+`"}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil || len(repo.lastUpdate.FederalStateID) == 0 { t.Fatalf("expected federal_state_id set") } }) t.Run("success update note", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() repo.rows[string(id)] = &icao.ICAO{ID: id, ICAOCode: "OLD", Note: "old"} h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"note":" updated note "}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } if repo.lastUpdate == nil || repo.lastUpdate.Note != "updated note" { t.Fatalf("expected note updated and trimmed") } }) t.Run("success fallback when refetch fails", func(t *testing.T) { repo := newMemICAORepo() id := uuidv7.MustBytes() existing := &icao.ICAO{ID: id, ICAOCode: "OLD"} repo.getByIDFn = func(_ []byte, call int) (*icao.ICAO, error) { if call == 1 { return existing, nil } return nil, errors.New("second lookup failed") } h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodPatch, "/api/v1/icao/update/"+idStr, []byte(`{"data":{"type":"icao","id":"`+idStr+`","attributes":{"address":"Only addr"}}}`)) if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } }) } func TestICAOHandlerDelete(t *testing.T) { t.Run("invalid uuid", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodDelete, "/api/v1/icao/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 := newMemICAORepo() repo.deleteErr = errors.New("delete failed") h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) id := mustUUIDStringICAO(t, uuidv7.MustBytes()) resp, _ := doJSONRequestICAO(t, app, http.MethodDelete, "/api/v1/icao/delete/"+id, nil) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400, got %d", resp.StatusCode) } }) t.Run("success", func(t *testing.T) { repo := newMemICAORepo() h := NewICAOHandler(service.NewICAOService(repo)) actor := uuidv7.MustBytes() app := newICAOTestApp(h, actor) id := uuidv7.MustBytes() idStr := mustUUIDStringICAO(t, id) resp, _ := doJSONRequestICAO(t, app, http.MethodDelete, "/api/v1/icao/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 TestICAOHandlerList(t *testing.T) { t.Run("list error", func(t *testing.T) { repo := newMemICAORepo() repo.listErr = errors.New("list failed") h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodGet, "/api/v1/icao/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 := newMemICAORepo() repo.listRows = []icao.ICAO{{ID: uuidv7.MustBytes(), ICAOCode: "A", SortKey: intPtrICAOHandler(3), IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now()}} repo.listTotal = 1 h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodGet, "/api/v1/icao/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, icao_code 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 := newMemICAORepo() repo.listRows = []icao.ICAO{} repo.listTotal = 0 h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodGet, "/api/v1/icao/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 TestICAOHandlerListDatatable(t *testing.T) { t.Run("list error", func(t *testing.T) { repo := newMemICAORepo() repo.listErr = errors.New("list failed") h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodGet, "/api/v1/icao/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 := newMemICAORepo() repo.listRows = []icao.ICAO{{ID: uuidv7.MustBytes(), ICAOCode: "A", CreatedAt: time.Now(), UpdatedAt: time.Now()}} repo.listTotal = 1 h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodGet, "/api/v1/icao/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 != 100 || 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 := newMemICAORepo() repo.listRows = []icao.ICAO{} repo.listTotal = 0 h := NewICAOHandler(service.NewICAOService(repo)) app := newICAOTestApp(h, nil) resp, _ := doJSONRequestICAO(t, app, http.MethodGet, "/api/v1/icao/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 TestICAOResource(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 := &icao.ICAO{ ID: id, ICAOCode: "EDDB", Name: "Berlin Brandenburg Airport", SortKey: intPtrICAOHandler(2), Address: "Street 1", LandlineNumber: "021123456", MobileNumber: "+491234", Email: "main@hic.local", IsActive: true, CreatedAt: now, CreatedBy: createdBy, UpdatedAt: now, UpdatedBy: updatedBy, DeletedAt: &deletedAt, DeletedBy: deletedBy, FederalState: &federal_state.FederalState{ Name: "Bayern", Land: &land.Land{Name: "Germany", LandISOCode: "DE"}, }, } res := icaoResource(row) if res.Type != "icao" { t.Fatalf("unexpected type: %s", res.Type) } if res.Attributes.SortKey == nil || *res.Attributes.SortKey != 2 || !res.Attributes.IsActive { t.Fatalf("expected sortkey and is_active in resource") } if res.Attributes.Name != "Berlin Brandenburg Airport" { t.Fatalf("expected name in resource") } if res.Attributes.FederalState != "Bayern" || res.Attributes.LandName != "Germany" || res.Attributes.LandISOCode != "DE" { t.Fatalf("expected federal and land fields in resource") } if res.ID == "" || res.Attributes.CreatedBy == "" || res.Attributes.UpdatedBy == "" || res.Attributes.DeletedBy == "" { t.Fatalf("expected UUID fields in resource") } if res.Attributes.CreatedAt == "" || res.Attributes.UpdatedAt == "" || res.Attributes.DeletedAt == "" { t.Fatalf("expected time fields in resource") } } func TestParseNullableUUID(t *testing.T) { t.Run("empty becomes nil", func(t *testing.T) { got, err := parseNullableUUID(" ") if err != nil { t.Fatalf("unexpected err: %v", err) } if got != nil { t.Fatalf("expected nil") } }) t.Run("valid uuid", func(t *testing.T) { id := uuidv7.MustBytes() idStr := mustUUIDStringICAO(t, id) got, err := parseNullableUUID(idStr) if err != nil { t.Fatalf("unexpected err: %v", err) } if string(got) != string(id) { t.Fatalf("expected same bytes") } }) t.Run("invalid uuid", func(t *testing.T) { if _, err := parseNullableUUID("not-uuid"); err == nil { t.Fatalf("expected err") } }) } func TestICAOFederalStateLandNameHelpers(t *testing.T) { if federalStateNameOrEmpty(nil) != "" || landNameOrEmpty(nil) != "" || landISOCodeOrEmpty(nil) != "" { t.Fatalf("expected empty for nil row") } rowWithoutFS := &icao.ICAO{} if federalStateNameOrEmpty(rowWithoutFS) != "" || landNameOrEmpty(rowWithoutFS) != "" || landISOCodeOrEmpty(rowWithoutFS) != "" { t.Fatalf("expected empty when relation missing") } rowWithoutLand := &icao.ICAO{FederalState: &federal_state.FederalState{Name: "Bayern"}} if federalStateNameOrEmpty(rowWithoutLand) != "Bayern" { t.Fatalf("expected federal state name") } if landNameOrEmpty(rowWithoutLand) != "" || landISOCodeOrEmpty(rowWithoutLand) != "" { t.Fatalf("expected empty land fields") } rowWithLand := &icao.ICAO{ FederalState: &federal_state.FederalState{ Name: "Bayern", Land: &land.Land{Name: "Germany", LandISOCode: "DE"}, }, } if federalStateNameOrEmpty(rowWithLand) != "Bayern" || landNameOrEmpty(rowWithLand) != "Germany" || landISOCodeOrEmpty(rowWithLand) != "DE" { t.Fatalf("expected names from relation") } if strings.TrimSpace(federalStateNameOrEmpty(rowWithLand)) == "" { t.Fatalf("sanity check non-empty federal state") } } func intPtrICAOHandler(v int) *int { return &v }