init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View File

@@ -0,0 +1,675 @@
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)
}
}