Files
fm_be/internal/service/flight_data_service_test.go
2026-07-16 22:16:45 +07:00

469 lines
16 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
afterflightinspection "wucher/internal/domain/after_flight_inspection"
flightdomain "wucher/internal/domain/flight"
flightdata "wucher/internal/domain/flight_data"
fmreport "wucher/internal/domain/fm_report"
missiondomain "wucher/internal/domain/mission"
reserveac "wucher/internal/domain/reserve_ac"
takeoverdomain "wucher/internal/domain/takeover"
sharedconst "wucher/internal/shared/const"
"wucher/internal/shared/pkg/uuidv7"
)
type flightDataRepoStub struct {
createFn func(context.Context, *flightdata.FlightData) error
updateFn func(context.Context, *flightdata.FlightData) error
getByIDFn func(context.Context, []byte) (*flightdata.FlightData, error)
getByMissionIDFn func(context.Context, []byte) (*flightdata.FlightData, error)
getSPODetailsByFlightID func(context.Context, []byte) (*flightdata.SPODetails, error)
}
type missionResolverStub struct {
flightID []byte
err error
}
func (m missionResolverStub) FlightIDByMissionID(context.Context, []byte) ([]byte, error) {
return append([]byte(nil), m.flightID...), m.err
}
type fmReportLookupStub struct {
report *fmreport.Report
err error
}
type flightLookupStub struct {
flight *flightdomain.Flight
err error
}
type afterFlightInspectionLookupStub struct {
row *afterflightinspection.AfterFlightInspection
err error
}
func (s fmReportLookupStub) GetByFlightID(context.Context, []byte) (*fmreport.Report, error) {
return s.report, s.err
}
func (s flightLookupStub) GetByID(context.Context, []byte) (*flightdomain.Flight, error) {
return s.flight, s.err
}
func (s afterFlightInspectionLookupStub) GetByFlightInspectionID(context.Context, []byte) (*afterflightinspection.AfterFlightInspection, error) {
return s.row, s.err
}
func (r *flightDataRepoStub) Create(ctx context.Context, row *flightdata.FlightData) error {
if r.createFn != nil {
return r.createFn(ctx, row)
}
return nil
}
func (r *flightDataRepoStub) Update(ctx context.Context, row *flightdata.FlightData) error {
if r.updateFn != nil {
return r.updateFn(ctx, row)
}
return nil
}
func (r *flightDataRepoStub) Delete(context.Context, []byte, []byte) error { return nil }
func (r *flightDataRepoStub) GetByID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
if r.getByIDFn != nil {
return r.getByIDFn(ctx, id)
}
return nil, nil
}
func (r *flightDataRepoStub) GetByFlightID(context.Context, []byte) (*flightdata.FlightData, error) {
return nil, nil
}
func (r *flightDataRepoStub) GetByMissionID(ctx context.Context, id []byte) (*flightdata.FlightData, error) {
if r.getByMissionIDFn != nil {
return r.getByMissionIDFn(ctx, id)
}
return nil, nil
}
func (r *flightDataRepoStub) ListByMissionID(context.Context, []byte) ([]flightdata.FlightData, error) {
return nil, nil
}
func (r *flightDataRepoStub) List(context.Context, string, []byte, []byte, int, int) ([]flightdata.FlightData, int64, error) {
return nil, 0, nil
}
func (r *flightDataRepoStub) GetSPODetailsByFlightDataID(ctx context.Context, id []byte) (*flightdata.SPODetails, error) {
if r.getSPODetailsByFlightID != nil {
return r.getSPODetailsByFlightID(ctx, id)
}
return nil, nil
}
func (r *flightDataRepoStub) UpsertSPODetails(context.Context, []byte, *flightdata.SPOUpsertData) error {
return nil
}
func TestFlightDataServiceCreateSyncsCompleteStatus(t *testing.T) {
var updates []*flightdata.FlightData
rowID := []byte("019ecc1111111111")
repo := &flightDataRepoStub{
getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
loaded := &flightdata.FlightData{
ID: id,
MissionID: []byte("1234567890123456"),
CoPilotID: []byte("2345678901234567"),
FromICAOID: []byte("3456789012345678"),
ToHospitalID: []byte("4567890123456789"),
Mission: &missiondomain.Mission{Type: sharedconst.MissionTypeCAT},
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
Status: flightdata.StatusInProgress,
}
if got := flightdata.DeriveStatus(loaded, nil); got != flightdata.StatusCompleted {
t.Fatalf("expected derived status complete, got %q", got)
}
return loaded, nil
},
updateFn: func(_ context.Context, row *flightdata.FlightData) error {
updates = append(updates, row)
return nil
},
}
svc := NewFlightDataService(repo)
if err := svc.syncStatus(context.Background(), rowID); err != nil {
t.Fatalf("syncStatus returned error: %v", err)
}
if len(updates) != 1 {
t.Fatalf("expected one status sync update, got %d", len(updates))
}
if updates[0].Status != flightdata.StatusCompleted {
t.Fatalf("expected sync update to complete status, got %q", updates[0].Status)
}
}
func TestFlightDataServiceCreateKeepsSPOIncompleteStatus(t *testing.T) {
rowID := []byte("019ecc1122222222")
repo := &flightDataRepoStub{
getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
loaded := &flightdata.FlightData{
ID: id,
MissionID: []byte("1234567890123456"),
CoPilotID: []byte("2345678901234567"),
FromICAOID: []byte("3456789012345678"),
ToHospitalID: []byte("4567890123456789"),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
Mission: &missiondomain.Mission{Type: sharedconst.MissionTypeSPO},
Status: flightdata.StatusInProgress,
}
if got := flightdata.DeriveStatus(loaded, nil); got != flightdata.StatusInProgress {
t.Fatalf("expected derived status on_progress, got %q", got)
}
return loaded, nil
},
getSPODetailsByFlightID: func(context.Context, []byte) (*flightdata.SPODetails, error) {
return &flightdata.SPODetails{}, nil
},
}
svc := NewFlightDataService(repo)
if err := svc.syncStatus(context.Background(), rowID); err != nil {
t.Fatalf("syncStatus returned error: %v", err)
}
}
func TestFlightDataServiceCreateSyncsCompleteStatusWithoutRotorBrakeCycleForCATAndNCO(t *testing.T) {
for _, tc := range []struct {
name string
typ string
}{
{name: "cat", typ: sharedconst.MissionTypeCAT},
{name: "nco", typ: sharedconst.MissionTypeNCO},
} {
t.Run(tc.name, func(t *testing.T) {
var updates []*flightdata.FlightData
rowID := []byte("019ecc1144444444")
repo := &flightDataRepoStub{
getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
loaded := &flightdata.FlightData{
ID: id,
MissionID: []byte("1234567890123456"),
CoPilotID: []byte("2345678901234567"),
FromICAOID: []byte("3456789012345678"),
ToHospitalID: []byte("4567890123456789"),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
Mission: &missiondomain.Mission{Type: tc.typ},
Status: flightdata.StatusInProgress,
}
if got := flightdata.DeriveStatus(loaded, nil); got != flightdata.StatusCompleted {
t.Fatalf("expected derived status complete, got %q", got)
}
return loaded, nil
},
updateFn: func(_ context.Context, row *flightdata.FlightData) error {
updates = append(updates, row)
return nil
},
}
svc := NewFlightDataService(repo)
if err := svc.syncStatus(context.Background(), rowID); err != nil {
t.Fatalf("syncStatus returned error: %v", err)
}
if len(updates) != 1 {
t.Fatalf("expected one status sync update, got %d", len(updates))
}
if updates[0].Status != flightdata.StatusCompleted {
t.Fatalf("expected sync update to complete status, got %q", updates[0].Status)
}
})
}
}
func TestFlightDataServiceSyncStatusFallsBackToMissionLookup(t *testing.T) {
var updates []*flightdata.FlightData
rowID := []byte("019ecc1133333333")
missionID := []byte("1234567890123456")
repo := &flightDataRepoStub{
getByIDFn: func(_ context.Context, id []byte) (*flightdata.FlightData, error) {
return &flightdata.FlightData{
ID: id,
MissionID: missionID,
CoPilotID: []byte("2345678901234567"),
FromHospitalID: []byte("3456789012345678"),
ToHospitalID: []byte("4567890123456789"),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
Status: flightdata.StatusInProgress,
}, nil
},
getByMissionIDFn: func(_ context.Context, _ []byte) (*flightdata.FlightData, error) {
return &flightdata.FlightData{
ID: rowID,
MissionID: missionID,
CoPilotID: []byte("2345678901234567"),
FromHospitalID: []byte("3456789012345678"),
ToHospitalID: []byte("4567890123456789"),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
Mission: &missiondomain.Mission{Type: sharedconst.MissionTypeCAT},
Status: flightdata.StatusInProgress,
}, nil
},
updateFn: func(_ context.Context, row *flightdata.FlightData) error {
updates = append(updates, row)
return nil
},
}
svc := NewFlightDataService(repo)
if err := svc.syncStatus(context.Background(), rowID); err != nil {
t.Fatalf("syncStatus returned error: %v", err)
}
if len(updates) != 1 {
t.Fatalf("expected one status sync update, got %d", len(updates))
}
if updates[0].Status != flightdata.StatusCompleted {
t.Fatalf("expected sync update to complete status, got %q", updates[0].Status)
}
}
func TestFlightDataServiceCreateBlockedWhenFMReportExists(t *testing.T) {
missionID := uuidv7.MustBytes()
repo := &flightDataRepoStub{
createFn: func(context.Context, *flightdata.FlightData) error {
t.Fatal("create should not be called when fm report already exists")
return nil
},
}
svc := NewFlightDataService(repo).
WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
WithFMReportLookup(fmReportLookupStub{report: &fmreport.Report{ID: uuidv7.MustBytes(), FlightID: uuidv7.MustBytes()}})
err := svc.Create(context.Background(), &flightdata.FlightData{
MissionID: missionID,
CoPilotID: uuidv7.MustBytes(),
FromHospitalID: uuidv7.MustBytes(),
ToICAOID: uuidv7.MustBytes(),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
})
if !errors.Is(err, flightdata.ErrCreateBlocked) {
t.Fatalf("expected create blocked, got %v", err)
}
}
func TestFlightDataServiceUpdateLockedWhenFMReportCompleted(t *testing.T) {
missionID := uuidv7.MustBytes()
repo := &flightDataRepoStub{
getByIDFn: func(context.Context, []byte) (*flightdata.FlightData, error) {
return &flightdata.FlightData{
ID: uuidv7.MustBytes(),
MissionID: missionID,
CoPilotID: uuidv7.MustBytes(),
FromHospitalID: uuidv7.MustBytes(),
ToICAOID: uuidv7.MustBytes(),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
}, nil
},
updateFn: func(context.Context, *flightdata.FlightData) error {
t.Fatal("update should not be called when fm report is completed")
return nil
},
}
completedAt := time.Now().UTC()
svc := NewFlightDataService(repo).
WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
WithFMReportLookup(fmReportLookupStub{report: &fmreport.Report{ID: uuidv7.MustBytes(), FlightID: uuidv7.MustBytes(), CompletedAt: &completedAt}})
err := svc.Update(context.Background(), &flightdata.FlightData{
ID: uuidv7.MustBytes(),
MissionID: missionID,
CoPilotID: uuidv7.MustBytes(),
FromHospitalID: uuidv7.MustBytes(),
ToICAOID: uuidv7.MustBytes(),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
})
if !errors.Is(err, flightdata.ErrLocked) {
t.Fatalf("expected locked error, got %v", err)
}
}
func TestFlightDataServiceCreateBlockedWhenAfterFlightInspectionExists(t *testing.T) {
missionID := uuidv7.MustBytes()
inspectionID := uuidv7.MustBytes()
repo := &flightDataRepoStub{
createFn: func(context.Context, *flightdata.FlightData) error {
t.Fatal("create should not be called when after flight inspection exists")
return nil
},
}
svc := NewFlightDataService(repo).
WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
WithFlightLookup(flightLookupStub{
flight: &flightdomain.Flight{
ID: uuidv7.MustBytes(),
Takeover: &takeoverdomain.TakeoverAc{
ReserveAc: &reserveac.ReserveAc{InspectionID: inspectionID},
},
},
}).
WithAfterFlightInspectionLookup(afterFlightInspectionLookupStub{row: &afterflightinspection.AfterFlightInspection{ID: uuidv7.MustBytes(), FlightInspectionID: inspectionID}})
err := svc.Create(context.Background(), &flightdata.FlightData{
MissionID: missionID,
CoPilotID: uuidv7.MustBytes(),
FromHospitalID: uuidv7.MustBytes(),
ToICAOID: uuidv7.MustBytes(),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
})
if !errors.Is(err, flightdata.ErrLockedAfterFlightInspection) {
t.Fatalf("expected after-flight inspection lock error, got %v", err)
}
}
func TestFlightDataServiceUpdateAllowedWhenAfterFlightInspectionExists(t *testing.T) {
missionID := uuidv7.MustBytes()
inspectionID := uuidv7.MustBytes()
updated := false
repo := &flightDataRepoStub{
getByIDFn: func(context.Context, []byte) (*flightdata.FlightData, error) {
return &flightdata.FlightData{
ID: uuidv7.MustBytes(),
MissionID: missionID,
CoPilotID: uuidv7.MustBytes(),
FromHospitalID: uuidv7.MustBytes(),
ToICAOID: uuidv7.MustBytes(),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
}, nil
},
updateFn: func(context.Context, *flightdata.FlightData) error {
updated = true
return nil
},
}
svc := NewFlightDataService(repo).
WithMissionResolver(missionResolverStub{flightID: uuidv7.MustBytes()}).
WithFlightLookup(flightLookupStub{
flight: &flightdomain.Flight{
ID: uuidv7.MustBytes(),
Takeover: &takeoverdomain.TakeoverAc{
ReserveAc: &reserveac.ReserveAc{InspectionID: inspectionID},
},
},
}).
WithAfterFlightInspectionLookup(afterFlightInspectionLookupStub{row: &afterflightinspection.AfterFlightInspection{ID: uuidv7.MustBytes(), FlightInspectionID: inspectionID}})
err := svc.Update(context.Background(), &flightdata.FlightData{
ID: uuidv7.MustBytes(),
MissionID: missionID,
CoPilotID: uuidv7.MustBytes(),
FromHospitalID: uuidv7.MustBytes(),
ToICAOID: uuidv7.MustBytes(),
FlightTakeOff: time.Now().UTC(),
FlightLanding: time.Now().UTC(),
FlightDuration: time.Hour,
LandingCount: 1,
RotorBrakeCycle: 1,
})
if err != nil {
t.Fatalf("expected update to be allowed, got %v", err)
}
if !updated {
t.Fatal("expected update to be called")
}
}
func TestFlightDataServiceUpdateRequiresRow(t *testing.T) {
svc := NewFlightDataService(&flightDataRepoStub{})
if err := svc.Update(context.Background(), nil); !errors.Is(err, flightdata.ErrRequired) {
t.Fatalf("expected ErrRequired, got %v", err)
}
}