357 lines
12 KiB
Go
357 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"wucher/internal/domain/flight"
|
|
fmreport "wucher/internal/domain/fm_report"
|
|
"wucher/internal/shared/pkg/appctx"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
)
|
|
|
|
type fmReportRepoMock struct {
|
|
upsertFn func(context.Context, *fmreport.Report) error
|
|
getByFlightFn func(context.Context, []byte) (*fmreport.Report, error)
|
|
maxReportSeqFn func(context.Context, []byte) (int, error)
|
|
setReportCodeFn func(context.Context, []byte, string) error
|
|
listFn func(context.Context, fmreport.ListFilter, int, int) ([]fmreport.Report, int64, error)
|
|
createFleetHistoryFn func(context.Context, *fmreport.FleetHistory) error
|
|
getFleetHistoryFn func(context.Context, []byte) (*fmreport.FleetHistory, error)
|
|
}
|
|
|
|
type fmReportUsageRefresherStub struct {
|
|
calls int
|
|
}
|
|
|
|
func (s *fmReportUsageRefresherStub) RefreshAll(context.Context) error {
|
|
s.calls++
|
|
return nil
|
|
}
|
|
|
|
func (m *fmReportRepoMock) MaxReportSeqByHelicopter(ctx context.Context, helicopterID []byte) (int, error) {
|
|
if m.maxReportSeqFn != nil {
|
|
return m.maxReportSeqFn(ctx, helicopterID)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func (m *fmReportRepoMock) SetReportCode(ctx context.Context, id []byte, code string) error {
|
|
if m.setReportCodeFn != nil {
|
|
return m.setReportCodeFn(ctx, id, code)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fmStrPtr(v string) *string { return &v }
|
|
|
|
func (m *fmReportRepoMock) CreateFleetHistory(ctx context.Context, row *fmreport.FleetHistory) error {
|
|
if m.createFleetHistoryFn != nil {
|
|
return m.createFleetHistoryFn(ctx, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *fmReportRepoMock) GetFleetHistoryByReportID(ctx context.Context, reportID []byte) (*fmreport.FleetHistory, error) {
|
|
if m.getFleetHistoryFn != nil {
|
|
return m.getFleetHistoryFn(ctx, reportID)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *fmReportRepoMock) Upsert(ctx context.Context, row *fmreport.Report) error {
|
|
if m.upsertFn != nil {
|
|
return m.upsertFn(ctx, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *fmReportRepoMock) GetByFlightID(ctx context.Context, flightID []byte) (*fmreport.Report, error) {
|
|
if m.getByFlightFn != nil {
|
|
return m.getByFlightFn(ctx, flightID)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *fmReportRepoMock) List(ctx context.Context, filter fmreport.ListFilter, limit, offset int) ([]fmreport.Report, int64, error) {
|
|
if m.listFn != nil {
|
|
return m.listFn(ctx, filter, limit, offset)
|
|
}
|
|
return nil, 0, nil
|
|
}
|
|
|
|
func TestFMReportServiceUpsertForFlightPreservesExistingMetrics(t *testing.T) {
|
|
flightID := uuidv7.MustBytes()
|
|
takeoverID := uuidv7.MustBytes()
|
|
inspectionID := uuidv7.MustBytes()
|
|
existingID := uuidv7.MustBytes()
|
|
userID := uuidv7.MustBytes()
|
|
refresher := &fmReportUsageRefresherStub{}
|
|
|
|
repo := &fmReportRepoMock{
|
|
getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
|
|
return &fmreport.Report{
|
|
ID: existingID,
|
|
FlightID: flightID,
|
|
TakeoverID: takeoverID,
|
|
FlightInspectionID: inspectionID,
|
|
Engine1GpcN1: fmStrPtr("old-e1-gpc"),
|
|
Engine1PtcN2: fmStrPtr("old-e1-ptc"),
|
|
Engine2GpcN1: fmStrPtr("old-e2-gpc"),
|
|
Engine2PtcN2: fmStrPtr("old-e2-ptc"),
|
|
CreatedAt: time.Date(2026, 6, 18, 10, 0, 0, 0, time.UTC),
|
|
CreatedBy: userID,
|
|
}, nil
|
|
},
|
|
upsertFn: func(_ context.Context, row *fmreport.Report) error {
|
|
if row.Engine1GpcN1 == nil || row.Engine1PtcN2 == nil || row.Engine2GpcN1 == nil || row.Engine2PtcN2 == nil {
|
|
t.Fatalf("expected metrics preserved, got %+v", row)
|
|
}
|
|
if *row.Engine1GpcN1 != "old-e1-gpc" || *row.Engine1PtcN2 != "old-e1-ptc" || *row.Engine2GpcN1 != "old-e2-gpc" || *row.Engine2PtcN2 != "old-e2-ptc" {
|
|
t.Fatalf("expected metrics preserved, got %+v", row)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
svc := NewFMReportService(repo).WithUsageRefresher(refresher)
|
|
ctx := appctx.WithUserID(context.Background(), userID)
|
|
|
|
got, err := svc.UpsertForFlight(ctx, &flight.Flight{ID: flightID, TakeoverAcID: takeoverID}, inspectionID)
|
|
if err != nil {
|
|
t.Fatalf("UpsertForFlight: %v", err)
|
|
}
|
|
if got == nil || got.Engine1GpcN1 == nil || got.Engine1PtcN2 == nil || got.Engine2GpcN1 == nil || got.Engine2PtcN2 == nil {
|
|
t.Fatalf("unexpected report: %+v", got)
|
|
}
|
|
if *got.Engine1GpcN1 != "old-e1-gpc" || *got.Engine1PtcN2 != "old-e1-ptc" || *got.Engine2GpcN1 != "old-e2-gpc" || *got.Engine2PtcN2 != "old-e2-ptc" {
|
|
t.Fatalf("unexpected report: %+v", got)
|
|
}
|
|
if refresher.calls != 0 {
|
|
t.Fatalf("expected no usage refresh on upsert, got %d", refresher.calls)
|
|
}
|
|
}
|
|
|
|
func TestFMReportServiceUpdateForFlight(t *testing.T) {
|
|
flightID := uuidv7.MustBytes()
|
|
reportID := uuidv7.MustBytes()
|
|
userID := uuidv7.MustBytes()
|
|
refresher := &fmReportUsageRefresherStub{}
|
|
|
|
repo := &fmReportRepoMock{
|
|
getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
|
|
return &fmreport.Report{
|
|
ID: reportID,
|
|
FlightID: flightID,
|
|
TakeoverID: uuidv7.MustBytes(),
|
|
FlightInspectionID: uuidv7.MustBytes(),
|
|
Engine1GpcN1: fmStrPtr("old-e1-gpc"),
|
|
Engine1PtcN2: fmStrPtr("old-e1-ptc"),
|
|
Engine2GpcN1: fmStrPtr("old-e2-gpc"),
|
|
Engine2PtcN2: fmStrPtr("old-e2-ptc"),
|
|
}, nil
|
|
},
|
|
upsertFn: func(ctx context.Context, row *fmreport.Report) error {
|
|
if row.Engine1GpcN1 == nil || row.Engine1PtcN2 == nil || row.Engine2GpcN1 == nil || row.Engine2PtcN2 == nil {
|
|
t.Fatalf("unexpected patched values: %+v", row)
|
|
}
|
|
if *row.Engine1GpcN1 != "new-e1-gpc" || *row.Engine1PtcN2 != "new-e1-ptc" || *row.Engine2GpcN1 != "new-e2-gpc" || *row.Engine2PtcN2 != "new-e2-ptc" {
|
|
t.Fatalf("unexpected patched values: %+v", row)
|
|
}
|
|
if got := appctx.GetUserID(ctx); string(got) != string(userID) {
|
|
t.Fatalf("expected updated by user to be preserved in context")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
svc := NewFMReportService(repo).WithUsageRefresher(refresher)
|
|
ctx := appctx.WithUserID(context.Background(), userID)
|
|
|
|
got, err := svc.UpdateForFlight(ctx, flightID, fmStrPtr("new-e1-gpc"), fmStrPtr("new-e1-ptc"), fmStrPtr("new-e2-gpc"), fmStrPtr("new-e2-ptc"))
|
|
if err != nil {
|
|
t.Fatalf("UpdateForFlight: %v", err)
|
|
}
|
|
if got == nil || got.Engine1GpcN1 == nil || got.Engine1PtcN2 == nil || got.Engine2GpcN1 == nil || got.Engine2PtcN2 == nil {
|
|
t.Fatalf("unexpected report: %+v", got)
|
|
}
|
|
if *got.Engine1GpcN1 != "new-e1-gpc" || *got.Engine1PtcN2 != "new-e1-ptc" || *got.Engine2GpcN1 != "new-e2-gpc" || *got.Engine2PtcN2 != "new-e2-ptc" {
|
|
t.Fatalf("unexpected report: %+v", got)
|
|
}
|
|
if refresher.calls != 0 {
|
|
t.Fatalf("expected no usage refresh on update, got %d", refresher.calls)
|
|
}
|
|
}
|
|
|
|
type fmReportCodeResolverMock struct {
|
|
helicopterID []byte
|
|
helicopterType string
|
|
}
|
|
|
|
func (m fmReportCodeResolverMock) ResolveHelicopterForReportCode(context.Context, []byte) ([]byte, string) {
|
|
return m.helicopterID, m.helicopterType
|
|
}
|
|
|
|
func TestFMReportServiceUpsertForFlightAssignsReportCode(t *testing.T) {
|
|
flightID := uuidv7.MustBytes()
|
|
takeoverID := uuidv7.MustBytes()
|
|
inspectionID := uuidv7.MustBytes()
|
|
helicopterID := uuidv7.MustBytes()
|
|
userID := uuidv7.MustBytes()
|
|
|
|
var saved *fmreport.Report
|
|
var setCode string
|
|
repo := &fmReportRepoMock{
|
|
getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
|
|
return nil, nil
|
|
},
|
|
maxReportSeqFn: func(_ context.Context, gotHeli []byte) (int, error) {
|
|
if string(gotHeli) != string(helicopterID) {
|
|
t.Fatalf("expected counter scoped to helicopter, got %x", gotHeli)
|
|
}
|
|
return 4, nil
|
|
},
|
|
upsertFn: func(_ context.Context, row *fmreport.Report) error {
|
|
saved = row
|
|
if string(row.HelicopterID) != string(helicopterID) {
|
|
t.Fatalf("expected helicopter id persisted, got %x", row.HelicopterID)
|
|
}
|
|
if row.ReportCode != nil {
|
|
t.Fatalf("expected report code unset on insert, got %q", *row.ReportCode)
|
|
}
|
|
if len(row.ID) != 16 {
|
|
row.ID = uuidv7.MustBytes()
|
|
}
|
|
return nil
|
|
},
|
|
setReportCodeFn: func(_ context.Context, _ []byte, code string) error {
|
|
setCode = code
|
|
return nil
|
|
},
|
|
}
|
|
svc := NewFMReportService(repo).WithReportCodeResolver(fmReportCodeResolverMock{
|
|
helicopterID: helicopterID,
|
|
helicopterType: "SN0901",
|
|
})
|
|
ctx := appctx.WithUserID(context.Background(), userID)
|
|
|
|
got, err := svc.UpsertForFlight(ctx, &flight.Flight{ID: flightID, TakeoverAcID: takeoverID}, inspectionID)
|
|
if err != nil {
|
|
t.Fatalf("UpsertForFlight: %v", err)
|
|
}
|
|
if saved == nil {
|
|
t.Fatalf("expected report upserted")
|
|
}
|
|
if setCode != "SN0901-0005" {
|
|
t.Fatalf("expected report code SN0901-0005 persisted, got %q", setCode)
|
|
}
|
|
if got == nil || got.ReportCode == nil || *got.ReportCode != "SN0901-0005" {
|
|
t.Fatalf("unexpected returned report: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFMReportServiceUpsertForFlightRetriesOnDuplicateReportCode(t *testing.T) {
|
|
flightID := uuidv7.MustBytes()
|
|
takeoverID := uuidv7.MustBytes()
|
|
inspectionID := uuidv7.MustBytes()
|
|
helicopterID := uuidv7.MustBytes()
|
|
|
|
maxCalls := 0
|
|
setCalls := 0
|
|
repo := &fmReportRepoMock{
|
|
getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
|
|
return nil, nil
|
|
},
|
|
maxReportSeqFn: func(context.Context, []byte) (int, error) {
|
|
// First read sees 4; after the concurrent winner committed SN0901-0005,
|
|
// the retry sees 5.
|
|
maxCalls++
|
|
if maxCalls == 1 {
|
|
return 4, nil
|
|
}
|
|
return 5, nil
|
|
},
|
|
setReportCodeFn: func(_ context.Context, _ []byte, code string) error {
|
|
setCalls++
|
|
if code == "SN0901-0005" {
|
|
return fmreport.ErrDuplicateReportCode
|
|
}
|
|
return nil
|
|
},
|
|
upsertFn: func(_ context.Context, row *fmreport.Report) error {
|
|
if len(row.ID) != 16 {
|
|
row.ID = uuidv7.MustBytes()
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
svc := NewFMReportService(repo).WithReportCodeResolver(fmReportCodeResolverMock{
|
|
helicopterID: helicopterID,
|
|
helicopterType: "SN0901",
|
|
})
|
|
|
|
got, err := svc.UpsertForFlight(context.Background(), &flight.Flight{ID: flightID, TakeoverAcID: takeoverID}, inspectionID)
|
|
if err != nil {
|
|
t.Fatalf("UpsertForFlight: %v", err)
|
|
}
|
|
if setCalls != 2 {
|
|
t.Fatalf("expected one retry after duplicate, got %d SetReportCode calls", setCalls)
|
|
}
|
|
if got == nil || got.ReportCode == nil || *got.ReportCode != "SN0901-0006" {
|
|
t.Fatalf("expected report code SN0901-0006 after retry, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFMReportServiceUpdateForFlightNotFound(t *testing.T) {
|
|
svc := NewFMReportService(&fmReportRepoMock{
|
|
getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
|
|
return nil, nil
|
|
},
|
|
})
|
|
if _, err := svc.UpdateForFlight(context.Background(), uuidv7.MustBytes(), fmStrPtr("a"), fmStrPtr("b"), fmStrPtr("c"), fmStrPtr("d")); err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestFMReportServiceCompleteSetsCompletionFieldsAndRefreshesUsage(t *testing.T) {
|
|
flightID := uuidv7.MustBytes()
|
|
reportID := uuidv7.MustBytes()
|
|
userID := uuidv7.MustBytes()
|
|
refresher := &fmReportUsageRefresherStub{}
|
|
|
|
repo := &fmReportRepoMock{
|
|
getByFlightFn: func(context.Context, []byte) (*fmreport.Report, error) {
|
|
return &fmreport.Report{
|
|
ID: reportID,
|
|
FlightID: flightID,
|
|
TakeoverID: uuidv7.MustBytes(),
|
|
FlightInspectionID: uuidv7.MustBytes(),
|
|
}, nil
|
|
},
|
|
upsertFn: func(ctx context.Context, row *fmreport.Report) error {
|
|
if row.CompletedAt == nil {
|
|
t.Fatal("expected completed_at to be set")
|
|
}
|
|
if len(row.CompletedBy) != 16 {
|
|
t.Fatal("expected completed_by to be set")
|
|
}
|
|
if got := appctx.GetUserID(ctx); string(got) != string(userID) {
|
|
t.Fatal("expected user id propagated in context")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
svc := NewFMReportService(repo).WithUsageRefresher(refresher)
|
|
ctx := appctx.WithUserID(context.Background(), userID)
|
|
|
|
got, err := svc.Complete(ctx, flightID)
|
|
if err != nil {
|
|
t.Fatalf("Complete: %v", err)
|
|
}
|
|
if got == nil || got.CompletedAt == nil || len(got.CompletedBy) != 16 {
|
|
t.Fatalf("unexpected completed report: %+v", got)
|
|
}
|
|
if refresher.calls != 1 {
|
|
t.Fatalf("expected one usage refresh, got %d", refresher.calls)
|
|
}
|
|
}
|