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

287 lines
9.8 KiB
Go

package service
import (
"context"
"testing"
"time"
fleethistory "wucher/internal/domain/fleet_history"
fleetstatus "wucher/internal/domain/fleet_status"
"wucher/internal/domain/helicopter"
helicopterusage "wucher/internal/domain/helicopter_usage"
)
type stubFleetRepo struct {
latest map[string]*fleetstatus.FleetStatus
missionHeli []byte
created *fleetstatus.FleetStatus
updated *fleetstatus.FleetStatus
deletedID []byte
servicedID []byte
}
func (s *stubFleetRepo) Create(_ context.Context, row *fleetstatus.FleetStatus) error {
s.created = row
return nil
}
func (s *stubFleetRepo) Update(_ context.Context, row *fleetstatus.FleetStatus) error {
s.updated = row
return nil
}
func (s *stubFleetRepo) Delete(_ context.Context, id, _ []byte) error { s.deletedID = id; return nil }
func (s *stubFleetRepo) GetByID(_ context.Context, _ []byte) (*fleetstatus.FleetStatus, error) {
return s.created, nil
}
func (s *stubFleetRepo) List(_ context.Context, _, _ string, _, _ int) ([]fleetstatus.FleetStatus, int64, error) {
return nil, 0, nil
}
func (s *stubFleetRepo) LatestByHelicopterIDs(_ context.Context, _ [][]byte) (map[string]*fleetstatus.FleetStatus, error) {
return s.latest, nil
}
func (s *stubFleetRepo) MarkServiced(_ context.Context, id []byte, _ time.Time, _ []byte) error {
s.servicedID = id
return nil
}
func (s *stubFleetRepo) ListServiceHistoryByHelicopterID(_ context.Context, _ []byte, _, _ int) ([]fleetstatus.FleetStatusServiceLog, int64, error) {
return nil, 0, nil
}
func (s *stubFleetRepo) HelicopterIDByMissionID(_ context.Context, _ []byte) ([]byte, error) {
return s.missionHeli, nil
}
func (s *stubFleetRepo) HelicopterIDsWithFleetStatus(_ context.Context) ([][]byte, error) {
out := make([][]byte, 0, len(s.latest))
for k := range s.latest {
out = append(out, []byte(k))
}
return out, nil
}
type stubFleetHelis struct {
heli *helicopter.Helicopter
updated *helicopter.Helicopter
}
func (s *stubFleetHelis) GetByID(_ context.Context, _ []byte) (*helicopter.Helicopter, error) {
return s.heli, nil
}
func (s *stubFleetHelis) Update(_ context.Context, h *helicopter.Helicopter) error {
s.updated = h
return nil
}
type stubFleetUsage struct {
usage *helicopterusage.HelicopterUsage
}
func (s *stubFleetUsage) GetByHelicopterID(_ context.Context, _ []byte) (*helicopterusage.HelicopterUsage, error) {
return s.usage, nil
}
func (s *stubFleetUsage) ReviseManualTotals(_ context.Context, _ []byte, _ helicopterusage.ManualSummaryInput, _ []byte) error {
return nil
}
func fleetUsage(airframeHours float64) *stubFleetUsage {
return &stubFleetUsage{usage: &helicopterusage.HelicopterUsage{TotalAirframeHours: airframeHours}}
}
type stubHistoryRepo struct{ recorded int }
func (s *stubHistoryRepo) Create(_ context.Context, _ *fleethistory.FleetHistory) error {
s.recorded++
return nil
}
func (s *stubHistoryRepo) ListByHelicopter(_ context.Context, _ []byte, _, _ int) ([]fleethistory.FleetHistory, int64, error) {
return nil, 0, nil
}
func (s *stubHistoryRepo) HelicopterIDByInspection(_ context.Context, _ []byte) ([]byte, error) {
return nil, nil
}
func newHistory() *FleetHistoryService { return NewFleetHistoryService(&stubHistoryRepo{}) }
func heliID() []byte { return []byte("0123456789abcdef") }
func fleetWith(schedDue string) *fleetstatus.FleetStatus {
return &fleetstatus.FleetStatus{
ID: []byte("fedcba9876543210"),
HelicopterID: heliID(),
MaintenanceSchedules: []fleetstatus.MaintenanceSchedule{
{InspectionType: fleetstatus.InspectionTypeAF, Due: schedDue},
},
}
}
func TestSyncAutomaticAOG_SetWhenOverdue(t *testing.T) {
row := fleetWith("100")
repo := &stubFleetRepo{
latest: map[string]*fleetstatus.FleetStatus{string(heliID()): row},
}
helis := &stubFleetHelis{heli: &helicopter.Helicopter{ID: heliID(), NR1: true, NR2: true}}
svc := NewFleetStatusService(repo, nil, helis, nil).WithHistory(newHistory()).WithUsage(fleetUsage(150))
if err := svc.RecalculateAOGByHelicopter(context.Background(), heliID()); err != nil {
t.Fatalf("err: %v", err)
}
if helis.updated == nil || !helis.updated.AirOnGround {
t.Fatal("expected helicopter set AOG (150h > 100h due)")
}
if helis.updated.AOGSource != helicopter.AOGSourceAuto {
t.Fatalf("expected auto AOG source, got %q", helis.updated.AOGSource)
}
}
func TestSyncAutomaticAOG_NotOverdueNotGrounded(t *testing.T) {
row := fleetWith("100")
repo := &stubFleetRepo{
latest: map[string]*fleetstatus.FleetStatus{string(heliID()): row},
}
helis := &stubFleetHelis{heli: &helicopter.Helicopter{ID: heliID(), NR1: true, NR2: true}}
svc := NewFleetStatusService(repo, nil, helis, nil).WithUsage(fleetUsage(50))
if err := svc.RecalculateAOGByHelicopter(context.Background(), heliID()); err != nil {
t.Fatalf("err: %v", err)
}
if helis.updated != nil {
t.Fatal("not overdue and not previously auto-AOG: should not touch helicopter")
}
}
func TestSyncAutomaticAOG_NoUsageDataNotGrounded(t *testing.T) {
row := fleetWith("100")
repo := &stubFleetRepo{
latest: map[string]*fleetstatus.FleetStatus{string(heliID()): row},
}
helis := &stubFleetHelis{heli: &helicopter.Helicopter{ID: heliID(), NR1: true, NR2: true}}
svc := NewFleetStatusService(repo, nil, helis, nil).WithUsage(&stubFleetUsage{usage: nil})
if err := svc.RecalculateAOGByHelicopter(context.Background(), heliID()); err != nil {
t.Fatalf("err: %v", err)
}
if helis.updated != nil {
t.Fatal("fresh aircraft with no usage data must not be auto-AOG on an hours due")
}
}
func TestSyncAutomaticAOG_ClearWhenNoLongerOverdue(t *testing.T) {
row := fleetWith("100")
repo := &stubFleetRepo{
latest: map[string]*fleetstatus.FleetStatus{string(heliID()): row},
}
helis := &stubFleetHelis{heli: &helicopter.Helicopter{
ID: heliID(), NR1: true, NR2: true,
AirOnGround: true, AOGSource: helicopter.AOGSourceAuto, AOGReason: "[auto_next_due] A/F due hours reached",
}}
svc := NewFleetStatusService(repo, nil, helis, nil).WithHistory(newHistory()).WithUsage(fleetUsage(50))
if err := svc.RecalculateAOGByHelicopter(context.Background(), heliID()); err != nil {
t.Fatalf("err: %v", err)
}
if helis.updated == nil || helis.updated.AirOnGround {
t.Fatal("auto-AOG should be cleared once no longer overdue")
}
}
func TestFleetStatusCreateUpdateGet(t *testing.T) {
repo := &stubFleetRepo{}
svc := NewFleetStatusService(repo, nil, nil, nil) // attachments nil -> simple path
ctx := context.Background()
row := &fleetstatus.FleetStatus{HelicopterID: heliID()}
if err := svc.Create(ctx, row); err != nil || repo.created != row {
t.Fatalf("Create: %v", err)
}
if err := svc.Update(ctx, row); err != nil || repo.updated != row {
t.Fatalf("Update: %v", err)
}
if _, err := svc.GetByID(ctx, heliID()); err != nil {
t.Fatalf("GetByID: %v", err)
}
}
func TestRecalculateAOGByMission(t *testing.T) {
row := fleetWith("100")
repo := &stubFleetRepo{
latest: map[string]*fleetstatus.FleetStatus{string(heliID()): row},
missionHeli: heliID(),
}
helis := &stubFleetHelis{heli: &helicopter.Helicopter{ID: heliID(), NR1: true, NR2: true}}
svc := NewFleetStatusService(repo, nil, helis, nil).WithUsage(fleetUsage(150))
if err := svc.RecalculateAOGByMission(context.Background(), []byte("mission0123456789")); err != nil {
t.Fatalf("err: %v", err)
}
if helis.updated == nil || !helis.updated.AirOnGround {
t.Fatal("mission recalc should set AOG")
}
// empty mission id -> no-op
if err := svc.RecalculateAOGByMission(context.Background(), nil); err != nil {
t.Fatalf("nil mission err: %v", err)
}
// no fleet status for helicopter -> no-op
if err := NewFleetStatusService(&stubFleetRepo{}, nil, helis, nil).RecalculateAOGByHelicopter(context.Background(), heliID()); err != nil {
t.Fatalf("no-fleet-status err: %v", err)
}
}
func TestRecalculateAllAOG(t *testing.T) {
row := fleetWith("100")
repo := &stubFleetRepo{
latest: map[string]*fleetstatus.FleetStatus{string(heliID()): row},
}
helis := &stubFleetHelis{heli: &helicopter.Helicopter{ID: heliID(), NR1: true, NR2: true}}
svc := NewFleetStatusService(repo, nil, helis, nil).WithUsage(fleetUsage(150))
n, err := svc.RecalculateAllAOG(context.Background())
if err != nil {
t.Fatalf("err: %v", err)
}
if n != 1 {
t.Fatalf("expected 1 helicopter recomputed, got %d", n)
}
if helis.updated == nil || !helis.updated.AirOnGround {
t.Fatal("overdue helicopter should have been set AOG by the sweep")
}
}
func TestGetSummaryByHelicopterID_EngineGating(t *testing.T) {
usage := &stubFleetUsage{usage: &helicopterusage.HelicopterUsage{
TotalAirframeHours: 6620,
TotalEngine1Hours: 8011,
TotalEngine2Hours: 5981,
}}
helis := &stubFleetHelis{heli: &helicopter.Helicopter{ID: heliID(), NR1: true, NR2: false}}
svc := NewFleetStatusService(&stubFleetRepo{}, nil, helis, nil).WithUsage(usage)
summary, err := svc.GetSummaryByHelicopterID(context.Background(), heliID())
if err != nil {
t.Fatalf("err: %v", err)
}
assertFloatPtr(t, "airframe", summary.Airframe.Hours, ptr(6620.0))
assertFloatPtr(t, "engine1", summary.Engine1.Hours, ptr(8011.0))
if summary.Engine2.Hours != nil {
t.Fatal("engine 2 absent (NR2 false) -> hours must be nil")
}
}
func TestFleetStatusSimpleDelegators(t *testing.T) {
repo := &stubFleetRepo{}
svc := NewFleetStatusService(repo, nil, nil, nil).WithHistory(nil)
ctx := context.Background()
if err := svc.Delete(ctx, []byte("id"), nil); err != nil || string(repo.deletedID) != "id" {
t.Fatalf("Delete: %v", err)
}
if _, _, err := svc.List(ctx, "", "", 0, 0); err != nil {
t.Fatalf("List: %v", err)
}
if _, err := svc.LatestByHelicopterIDs(ctx, [][]byte{heliID()}); err != nil {
t.Fatalf("LatestByHelicopterIDs: %v", err)
}
if _, _, err := svc.ListServiceHistoryByHelicopterID(ctx, heliID(), 0, 0); err != nil {
t.Fatalf("ListServiceHistory: %v", err)
}
if err := svc.MarkServiced(ctx, []byte("ms"), time.Time{}, nil); err != nil || string(repo.servicedID) != "ms" {
t.Fatalf("MarkServiced: %v", err)
}
}