469 lines
16 KiB
Go
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)
|
|
}
|
|
}
|