package handlers import ( "bytes" "context" "errors" "io" "net/http" "strings" "testing" "time" "github.com/gofiber/fiber/v2" afterflightinspection "wucher/internal/domain/after_flight_inspection" "wucher/internal/domain/flight" flightdata "wucher/internal/domain/flight_data" flightinspection "wucher/internal/domain/flight_inspection" "wucher/internal/domain/mission" reserveac "wucher/internal/domain/reserve_ac" takeoverdomain "wucher/internal/domain/takeover" "wucher/internal/shared/pkg/uuidv7" ) type flightServiceMock struct { createFn func(context.Context, *flight.Flight) error getByIDFn func(context.Context, []byte) (*flight.Flight, error) listByUserIDFn func(context.Context, []byte, int, int) ([]flight.Flight, int64, error) listFn func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error) } type missionServiceMock struct { listByFlightIDFn func(context.Context, []byte) ([]mission.Mission, error) listByFlightIDsFn func(context.Context, [][]byte) ([]mission.Mission, error) listFn func(context.Context, string, string, []byte, int, int) ([]mission.Mission, int64, error) listDTFn func(context.Context, mission.ListFilter, int, int) ([]mission.Mission, int64, int64, error) deleteByIDFn func(context.Context, []byte, []byte) error } type afterFlightInspectionServiceMock struct { getByFlightInspectionIDFn func(context.Context, []byte) (*afterflightinspection.AfterFlightInspection, error) } type takeoverServiceMock struct { getByIDFn func(context.Context, []byte) (*takeoverdomain.TakeoverAc, error) } func (m *flightServiceMock) Create(ctx context.Context, row *flight.Flight) error { if m.createFn != nil { return m.createFn(ctx, row) } return nil } func (m *flightServiceMock) Update(context.Context, *flight.Flight) error { return nil } func (m *flightServiceMock) Delete(context.Context, []byte, []byte) error { return nil } func (m *flightServiceMock) GetByID(ctx context.Context, id []byte) (*flight.Flight, error) { if m.getByIDFn != nil { return m.getByIDFn(ctx, id) } return nil, nil } func (m *flightServiceMock) ListByUserID(ctx context.Context, userID []byte, limit, offset int) ([]flight.Flight, int64, error) { if m.listByUserIDFn != nil { return m.listByUserIDFn(ctx, userID, limit, offset) } return nil, 0, nil } func (m *flightServiceMock) ListByCreatedBy(context.Context, []byte, string, string, string, int, int) ([]flight.Flight, int64, error) { return nil, 0, nil } func (m *flightServiceMock) GetByReserveAcID(context.Context, []byte) (*flight.Flight, error) { return nil, nil } func (m *flightServiceMock) ListByReserveAcID(context.Context, []byte) ([]flight.Flight, error) { return nil, nil } func (m *flightServiceMock) GetByTakeoverAcID(context.Context, []byte) (*flight.Flight, error) { return nil, nil } func (m *flightServiceMock) ListByTakeoverAcIDs(context.Context, [][]byte) ([]flight.Flight, error) { return nil, nil } func (m *flightServiceMock) GetByDutyRosterID(context.Context, []byte) (*flight.Flight, error) { return nil, nil } func (m *flightServiceMock) ListByDutyRosterIDs(context.Context, [][]byte) ([]flight.Flight, error) { return nil, nil } func (m *flightServiceMock) List(ctx context.Context, filter string, date string, sort string, limit, offset int) ([]flight.Flight, int64, error) { if m.listFn != nil { return m.listFn(ctx, filter, date, sort, limit, offset) } return nil, 0, nil } func (m *missionServiceMock) Create(context.Context, *mission.Mission) error { return nil } func (m *missionServiceMock) CreateType(context.Context, *mission.MissionCategory) error { return nil } func (m *missionServiceMock) UpdateByID(context.Context, []byte, string, []byte, string, []byte) error { return nil } func (m *missionServiceMock) DeleteByFlightID(context.Context, []byte, []byte) error { return nil } func (m *missionServiceMock) AttachFile(context.Context, []byte, []byte) error { return nil } func (m *missionServiceMock) DetachFile(context.Context, []byte, []byte) error { return nil } func (m *missionServiceMock) DeleteByID(ctx context.Context, missionID []byte, deletedBy []byte) error { if m.deleteByIDFn != nil { return m.deleteByIDFn(ctx, missionID, deletedBy) } return nil } func (m *missionServiceMock) ListByFlightID(ctx context.Context, flightID []byte) ([]mission.Mission, error) { if m.listByFlightIDFn != nil { return m.listByFlightIDFn(ctx, flightID) } return nil, nil } func (m *missionServiceMock) ListByFlightIDs(ctx context.Context, flightIDs [][]byte) ([]mission.Mission, error) { if m.listByFlightIDsFn != nil { return m.listByFlightIDsFn(ctx, flightIDs) } return nil, nil } func (m *missionServiceMock) GetByID(context.Context, []byte) (*mission.Mission, error) { return nil, nil } func (m *missionServiceMock) GetByFlightID(context.Context, []byte) (*mission.Mission, error) { return nil, nil } func (m *missionServiceMock) List(ctx context.Context, filter string, sort string, flightID []byte, flightDataStatus string, limit, offset int) ([]mission.Mission, int64, error) { if m.listFn != nil { return m.listFn(ctx, filter, sort, flightID, limit, offset) } return nil, 0, nil } func (m *missionServiceMock) ListDatatable(ctx context.Context, filter mission.ListFilter, limit, offset int) ([]mission.Mission, int64, int64, error) { if m.listDTFn != nil { return m.listDTFn(ctx, filter, limit, offset) } return nil, 0, 0, nil } func (m *missionServiceMock) ListTypes(context.Context) ([]mission.MissionCategory, error) { return nil, nil } func (m *afterFlightInspectionServiceMock) Upsert(context.Context, []byte, *afterflightinspection.UpsertRequest) (*afterflightinspection.AfterFlightInspection, error) { return nil, nil } func (m *afterFlightInspectionServiceMock) GetByFlightInspectionID(ctx context.Context, id []byte) (*afterflightinspection.AfterFlightInspection, error) { if m.getByFlightInspectionIDFn != nil { return m.getByFlightInspectionIDFn(ctx, id) } return nil, nil } func (m *afterFlightInspectionServiceMock) DeleteByFlightInspectionID(context.Context, []byte) error { return nil } func (m *takeoverServiceMock) Create(context.Context, *takeoverdomain.TakeoverAc) error { return nil } func (m *takeoverServiceMock) Update(context.Context, *takeoverdomain.TakeoverAc) error { return nil } func (m *takeoverServiceMock) Delete(context.Context, []byte, []byte) error { return nil } func (m *takeoverServiceMock) AttachFile(context.Context, []byte, []byte) error { return nil } func (m *takeoverServiceMock) GetByID(ctx context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) { if m.getByIDFn != nil { return m.getByIDFn(ctx, id) } return nil, nil } func (m *takeoverServiceMock) List(context.Context, int, int) ([]takeoverdomain.TakeoverAc, int64, error) { return nil, 0, nil } func newFlightApp(h *FlightHandler, withActor bool) *fiber.App { app := fiber.New() app.Use(func(c *fiber.Ctx) error { if withActor { c.Locals("user_id", uuidv7.MustBytes()) } return c.Next() }) app.Post("/flights", h.Create) app.Get("/flights/me", h.GetByAuthUser) app.Get("/flights", h.List) return app } func doReqFlight(t *testing.T, app *fiber.App, method, path string, body []byte) (int, string) { t.Helper() var rdr io.Reader if body != nil { rdr = bytes.NewReader(body) } req, err := http.NewRequest(method, path, rdr) if err != nil { t.Fatalf("new req: %v", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := app.Test(req) if err != nil { t.Fatalf("app.Test: %v", err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(b) } func TestFlightHandler(t *testing.T) { t.Run("new and with reserve ac", func(t *testing.T) { h := NewFlightHandler(&flightServiceMock{}) if h == nil || h.validate == nil { t.Fatal("expected handler initialized") } if h.WithReserveAcService(nil) != h { t.Fatal("expected same pointer") } }) t.Run("create invalid json", func(t *testing.T) { h := NewFlightHandler(&flightServiceMock{}) code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", []byte(`{"data":`)) if code != http.StatusBadRequest { t.Fatalf("expected 400 got %d", code) } }) t.Run("create validation", func(t *testing.T) { h := NewFlightHandler(&flightServiceMock{}) code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", []byte(`{}`)) if code != http.StatusUnprocessableEntity { t.Fatalf("expected 422 got %d", code) } }) t.Run("create date invalid", func(t *testing.T) { h := NewFlightHandler(&flightServiceMock{}) body := []byte(`{"data":{"type":"flight","attributes":{"date":"x"}}}`) code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body) if code != http.StatusUnprocessableEntity { t.Fatalf("expected 422 got %d", code) } }) t.Run("create optional uuid invalid", func(t *testing.T) { h := NewFlightHandler(&flightServiceMock{}) body := []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14","duty_roster_id":"bad"}}}`) code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body) if code != http.StatusUnprocessableEntity { t.Fatalf("expected 422 got %d", code) } body = []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14","reserve_ac_id":"bad"}}}`) code, _ = doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body) if code != http.StatusUnprocessableEntity { t.Fatalf("expected 422 got %d", code) } }) t.Run("create service error", func(t *testing.T) { h := NewFlightHandler(&flightServiceMock{ createFn: func(context.Context, *flight.Flight) error { return errors.New("boom") }, }) body := []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14"}}}`) code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body) if code != http.StatusBadRequest { t.Fatalf("expected 400 got %d", code) } }) t.Run("create success with refetch and fallback", func(t *testing.T) { var createdID []byte h := NewFlightHandler(&flightServiceMock{ createFn: func(_ context.Context, row *flight.Flight) error { row.ID = uuidv7.MustBytes() createdID = append([]byte(nil), row.ID...) return nil }, getByIDFn: func(_ context.Context, id []byte) (*flight.Flight, error) { if string(id) != string(createdID) { t.Fatal("unexpected id") } return &flight.Flight{ ID: id, Date: time.Date(2026, 4, 14, 0, 0, 0, 0, time.UTC), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), }, nil }, }) body := []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14","duty_roster_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `","reserve_ac_id":"` + mustUUIDStringHospital(t, uuidv7.MustBytes()) + `"}}}`) code, _ := doReqFlight(t, newFlightApp(h, true), http.MethodPost, "/flights", body) if code != http.StatusCreated { t.Fatalf("expected 201 got %d", code) } h2 := NewFlightHandler(&flightServiceMock{ createFn: func(_ context.Context, row *flight.Flight) error { row.ID = uuidv7.MustBytes() row.CreatedAt = time.Now().UTC() row.UpdatedAt = row.CreatedAt return nil }, getByIDFn: func(context.Context, []byte) (*flight.Flight, error) { return nil, errors.New("x") }, }) code, _ = doReqFlight(t, newFlightApp(h2, true), http.MethodPost, "/flights", []byte(`{"data":{"type":"flight","attributes":{"date":"2026-04-14"}}}`)) if code != http.StatusCreated { t.Fatalf("expected 201 got %d", code) } }) t.Run("get by auth user branches", func(t *testing.T) { h := NewFlightHandler(&flightServiceMock{}) code, _ := doReqFlight(t, newFlightApp(h, false), http.MethodGet, "/flights/me", nil) if code != http.StatusUnauthorized { t.Fatalf("expected 401 got %d", code) } hErr := NewFlightHandler(&flightServiceMock{ listByUserIDFn: func(context.Context, []byte, int, int) ([]flight.Flight, int64, error) { return nil, 0, errors.New("err") }, }) code, _ = doReqFlight(t, newFlightApp(hErr, true), http.MethodGet, "/flights/me?page[number]=0&page[size]=500", nil) if code != http.StatusBadRequest { t.Fatalf("expected 400 got %d", code) } row := flight.Flight{ ID: uuidv7.MustBytes(), Date: time.Date(2026, 4, 14, 0, 0, 0, 0, time.UTC), DutyRosterID: uuidv7.MustBytes(), ReserveAcID: uuidv7.MustBytes(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } hOK := NewFlightHandler(&flightServiceMock{ listByUserIDFn: func(_ context.Context, _ []byte, limit, offset int) ([]flight.Flight, int64, error) { if limit != 100 || offset != 0 { t.Fatalf("unexpected paging %d %d", limit, offset) } return []flight.Flight{row}, 1, nil }, }) code, body := doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights/me?page[number]=0&page[size]=500", nil) if code != http.StatusOK || !strings.Contains(body, `"flight"`) { t.Fatalf("expected 200 flight body, got %d %s", code, body) } hPg := NewFlightHandler(&flightServiceMock{ listByUserIDFn: func(_ context.Context, _ []byte, limit, offset int) ([]flight.Flight, int64, error) { if limit <= 0 || offset != 0 { t.Fatalf("unexpected paging") } return nil, 0, nil }, }) code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=1&limit=5", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=1&size=7", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=-1&page[size]=-1", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } code, _ = doReqFlight(t, newFlightApp(hPg, true), http.MethodGet, "/flights/me?page=1&page[size]=-1&limit=-1&size=-1", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } }) t.Run("list branches", func(t *testing.T) { hErr := NewFlightHandler(&flightServiceMock{ listFn: func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error) { return nil, 0, errors.New("no") }, }) code, _ := doReqFlight(t, newFlightApp(hErr, true), http.MethodGet, "/flights?search=x", nil) if code != http.StatusBadRequest { t.Fatalf("expected 400 got %d", code) } hOK := NewFlightHandler(&flightServiceMock{ listFn: func(_ context.Context, filter, date, sort string, limit, offset int) ([]flight.Flight, int64, error) { if filter != "abc" || sort != "-date" || limit != 0 || offset != 0 { t.Fatalf("unexpected args") } return []flight.Flight{{ID: uuidv7.MustBytes(), Date: time.Now().UTC(), CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}}, 1, nil }, }).WithMissionService(&missionServiceMock{ listByFlightIDFn: func(_ context.Context, _ []byte) ([]mission.Mission, error) { return []mission.Mission{{ID: uuidv7.MustBytes(), Type: "CAT", CreatedAt: time.Now().UTC()}}, nil }, }) code, body := doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?filter[search]=abc&sort=-date&page[number]=0&page[size]=0", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } if !strings.Contains(body, `"mission_id"`) { t.Fatalf("expected mission_id in response, got %s", body) } hProgress := NewFlightHandler(&flightServiceMock{ listFn: func(_ context.Context, filter, date, sort string, limit, offset int) ([]flight.Flight, int64, error) { if filter != "" || date != "" || sort != "" || limit != 0 || offset != 0 { t.Fatalf("unexpected args") } return []flight.Flight{{ ID: uuidv7.MustBytes(), Date: time.Now().UTC(), TakeoverAcID: uuidv7.MustBytes(), Takeover: &takeoverdomain.TakeoverAc{ ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{Status: "onprogress"}, }, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), }}, 1, nil }, }) code, body = doReqFlight(t, newFlightApp(hProgress, true), http.MethodGet, "/flights", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } if !strings.Contains(body, `"status":"in_progress"`) { t.Fatalf("expected on_progress status, got %s", body) } if !strings.Contains(body, `"complete":false`) { t.Fatalf("expected complete=false in response, got %s", body) } inspectionID := uuidv7.MustBytes() afterID := uuidv7.MustBytes() hAfter := NewFlightHandler(&flightServiceMock{ listFn: func(_ context.Context, filter, date, sort string, limit, offset int) ([]flight.Flight, int64, error) { if filter != "" || date != "" || sort != "" || limit != 0 || offset != 0 { t.Fatalf("unexpected args") } return []flight.Flight{{ ID: uuidv7.MustBytes(), Date: time.Now().UTC(), TakeoverAcID: uuidv7.MustBytes(), Takeover: &takeoverdomain.TakeoverAc{ ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{ Inspection: &flightinspection.FlightInspection{ID: inspectionID}, }, }, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), }}, 1, nil }, }).WithMissionService(&missionServiceMock{ listByFlightIDFn: func(_ context.Context, _ []byte) ([]mission.Mission, error) { return []mission.Mission{{ID: uuidv7.MustBytes(), Type: "CAT", CreatedAt: time.Now().UTC()}}, nil }, }).WithAfterFlightInspectionService(&afterFlightInspectionServiceMock{ getByFlightInspectionIDFn: func(_ context.Context, id []byte) (*afterflightinspection.AfterFlightInspection, error) { if string(id) != string(inspectionID) { t.Fatalf("unexpected inspection id") } now := time.Now().UTC() return &afterflightinspection.AfterFlightInspection{ ID: afterID, FlightInspectionID: inspectionID, CreatedAt: now, CreatedBy: uuidv7.MustBytes(), UpdatedAt: now, UpdatedBy: uuidv7.MustBytes(), }, nil }, }).WithTakeoverService(&takeoverServiceMock{ getByIDFn: func(_ context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) { if len(id) != 16 { t.Fatalf("unexpected takeover id") } return &takeoverdomain.TakeoverAc{ ID: id, BaseID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{ AircraftID: uuidv7.MustBytes(), Inspection: &flightinspection.FlightInspection{ID: inspectionID}, }, RosterCrews: []takeoverdomain.TakeoverRosterCrew{{ID: uuidv7.MustBytes()}}, OtherPeople: []takeoverdomain.TakeoverOtherPerson{{ID: uuidv7.MustBytes()}}, }, nil }, }) code, body = doReqFlight(t, newFlightApp(hAfter, true), http.MethodGet, "/flights", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } if !strings.Contains(body, `"phase":"after_flight_inspection"`) { t.Fatalf("expected after_flight_inspection step, got %s", body) } if !strings.Contains(body, `"step":3`) { t.Fatalf("expected step 3 in response, got %s", body) } if !strings.Contains(body, `"complete":true`) { t.Fatalf("expected complete=true in response, got %s", body) } code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&limit=5&search=abc&sort=-date", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&size=7&search=abc&sort=-date", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=-1&page[size]=-1&search=abc&sort=-date", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&page[size]=-1&limit=-1&size=-1&search=abc&sort=-date", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } code, _ = doReqFlight(t, newFlightApp(hOK, true), http.MethodGet, "/flights?page=1&page[size]=200&search=abc&sort=-date", nil) if code != http.StatusOK { t.Fatalf("expected 200 got %d", code) } }) t.Run("helpers", func(t *testing.T) { if stringPtrOrEmpty(nil) != "" { t.Fatal("expected empty") } v := " x " if stringPtrOrEmpty(&v) != "x" { t.Fatal("expected trimmed") } if timeStringOrEmptyRefTime(nil) != "" || timeStringOrEmptyRefTime(&time.Time{}) != "" { t.Fatal("expected empty") } now := time.Now().UTC() if timeStringOrEmptyRefTime(&now) == "" { t.Fatal("expected time") } raw := "bad" if _, errObj := parseOptionalUUIDFlight(&raw, "/data/attributes/duty_roster_id"); errObj == nil { t.Fatal("expected err") } raw = " " if id, errObj := parseOptionalUUIDFlight(&raw, "/data/attributes/duty_roster_id"); errObj != nil || id != nil { t.Fatal("expected nil") } raw = mustUUIDStringHospital(t, uuidv7.MustBytes()) if id, errObj := parseOptionalUUIDFlight(&raw, "/data/attributes/duty_roster_id"); errObj != nil || len(id) != 16 { t.Fatal("expected id") } if s := flightMissionStep(nil); s.Status != "unassigned" { t.Fatal("expected unassigned") } if s := flightDutyRosterStep(nil); s.Status != "unassigned" { t.Fatal("expected unassigned") } if s := flightReserveAcStep(nil); s.Status != "unassigned" { t.Fatal("expected unassigned") } if s := flightReserveAcStep(&flight.Flight{Takeover: &takeoverdomain.TakeoverAc{ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{Status: "pending"}}}); s.Status != "pending" { t.Fatal("expected pending") } if s := flightReserveAcStep(&flight.Flight{Takeover: &takeoverdomain.TakeoverAc{ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{Status: "onprogress"}}}); s.Status != "in_progress" { t.Fatal("expected on progress") } if s := flightReserveAcStep(&flight.Flight{Takeover: &takeoverdomain.TakeoverAc{ReserveAcID: uuidv7.MustBytes(), ReserveAc: &reserveac.ReserveAc{Status: "approved", UpdatedAt: time.Now().UTC(), UpdatedBy: uuidv7.MustBytes()}}}); s.Status != "completed" { t.Fatal("expected completed") } if s := flightMissionStep(&flight.Flight{}); s.Status != "unassigned" { t.Fatal("expected mission unassigned") } }) } func TestFlightHandlerList_MissionStepShowsInProgressUntilFlightDataComplete(t *testing.T) { flightID := uuidv7.MustBytes() missionID := uuidv7.MustBytes() h := NewFlightHandler(&flightServiceMock{ listFn: func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error) { return []flight.Flight{{ ID: flightID, Date: time.Date(2026, time.June, 26, 0, 0, 0, 0, time.UTC), CreatedAt: time.Date(2026, time.June, 24, 4, 31, 37, 0, time.UTC), CreatedBy: []byte("creator-id-12345"), }}, 1, nil }, }).WithMissionService(&missionServiceMock{ listByFlightIDFn: func(context.Context, []byte) ([]mission.Mission, error) { return []mission.Mission{{ ID: missionID, FlightID: flightID, Type: "CAT", CreatedAt: time.Date(2026, time.June, 24, 5, 23, 3, 0, time.UTC), UpdatedAt: time.Date(2026, time.June, 24, 5, 23, 3, 0, time.UTC), }}, nil }, }).WithFlightDataService(&flightDataServiceMock{ getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return &flightdata.FlightData{Status: flightdata.StatusInProgress}, nil }, }) app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_id", uuidv7.MustBytes()) return c.Next() }) app.Get("/flights", h.List) code, body := doReqFlight(t, app, http.MethodGet, "/flights?date=2026-06-26&limit=2000", nil) if code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", code, body) } if !strings.Contains(body, `"phase":"mission"`) { t.Fatalf("expected mission step in body: %s", body) } if !strings.Contains(body, `"status":"in_progress"`) { t.Fatalf("expected mission step in_progress: %s", body) } } func TestFlightHandlerList_MissionStepShowsCompletedWhenFlightDataComplete(t *testing.T) { flightID := uuidv7.MustBytes() missionID := uuidv7.MustBytes() h := NewFlightHandler(&flightServiceMock{ listFn: func(context.Context, string, string, string, int, int) ([]flight.Flight, int64, error) { return []flight.Flight{{ ID: flightID, Date: time.Date(2026, time.June, 26, 0, 0, 0, 0, time.UTC), CreatedAt: time.Date(2026, time.June, 24, 4, 31, 37, 0, time.UTC), CreatedBy: []byte("creator-id-12345"), }}, 1, nil }, }).WithMissionService(&missionServiceMock{ listByFlightIDFn: func(context.Context, []byte) ([]mission.Mission, error) { return []mission.Mission{{ ID: missionID, FlightID: flightID, Type: "CAT", CreatedAt: time.Date(2026, time.June, 24, 5, 23, 3, 0, time.UTC), UpdatedAt: time.Date(2026, time.June, 24, 6, 41, 45, 0, time.UTC), UpdatedBy: []byte("test test"), }}, nil }, }).WithFlightDataService(&flightDataServiceMock{ getByMisFn: func(context.Context, []byte) (*flightdata.FlightData, error) { return &flightdata.FlightData{Status: flightdata.StatusCompleted}, nil }, }) app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("user_id", uuidv7.MustBytes()) return c.Next() }) app.Get("/flights", h.List) code, body := doReqFlight(t, app, http.MethodGet, "/flights?date=2026-06-26&limit=2000", nil) if code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", code, body) } if !strings.Contains(body, `"phase":"mission"`) { t.Fatalf("expected mission step in body: %s", body) } if !strings.Contains(body, `"status":"completed"`) { t.Fatalf("expected mission step completed: %s", body) } }