287 lines
9.8 KiB
Go
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)
|
|
}
|
|
}
|