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,356 @@
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)
}
}