init push
This commit is contained in:
741
internal/service/takeover_service_test.go
Normal file
741
internal/service/takeover_service_test.go
Normal file
@@ -0,0 +1,741 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/domain/base"
|
||||
beforeflightinspection "wucher/internal/domain/before_flight_inspection"
|
||||
filemanager "wucher/internal/domain/file_manager"
|
||||
flightinspection "wucher/internal/domain/flight_inspection"
|
||||
flightprepcheck "wucher/internal/domain/flight_prep_check"
|
||||
helicopterfile "wucher/internal/domain/helicopter_file"
|
||||
"wucher/internal/repository/mysql"
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
requestdto "wucher/internal/transport/http/dto/request"
|
||||
)
|
||||
|
||||
func openTakeoverServiceTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", filepath.Join(t.TempDir(), "takeover.db"))
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&base.BaseCategory{}, &base.Base{}, &base.BaseOperationalShiftTime{}); err != nil {
|
||||
t.Fatalf("auto migrate base models: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&flightinspection.FlightInspection{}, &beforeflightinspection.BeforeFlightInspection{}, &flightprepcheck.FlightPrepCheck{}); err != nil {
|
||||
t.Fatalf("auto migrate inspection models: %v", err)
|
||||
}
|
||||
if err := db.Exec(`CREATE TABLE helicopter_files (
|
||||
id BLOB PRIMARY KEY,
|
||||
section TEXT NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 1,
|
||||
is_mandatory INTEGER NOT NULL DEFAULT 0,
|
||||
helicopter_id BLOB NOT NULL,
|
||||
file_attachment_id BLOB,
|
||||
source_file_id BLOB,
|
||||
created_at DATETIME,
|
||||
created_by BLOB,
|
||||
updated_at DATETIME,
|
||||
updated_by BLOB
|
||||
)`).Error; err != nil {
|
||||
t.Fatalf("create helicopter_files table: %v", err)
|
||||
}
|
||||
if err := db.Exec(`CREATE TABLE flight_inspection_file_checklists (
|
||||
id BLOB PRIMARY KEY,
|
||||
is_done INTEGER NOT NULL DEFAULT 0,
|
||||
helicopter_file_id BLOB NOT NULL,
|
||||
flight_inspection_id BLOB NOT NULL,
|
||||
created_at DATETIME,
|
||||
created_by BLOB,
|
||||
updated_at DATETIME,
|
||||
updated_by BLOB,
|
||||
UNIQUE (flight_inspection_id, helicopter_file_id)
|
||||
)`).Error; err != nil {
|
||||
t.Fatalf("create flight_inspection_file_checklists table: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// newChecklistTestService builds a TakeoverService wired with the concrete repository so
|
||||
// the checklist upsert can load template files (DB access lives in the repository layer).
|
||||
func newChecklistTestService(db *gorm.DB) *TakeoverService {
|
||||
return NewTakeoverService(db).WithChecklistTemplateRepository(mysql.NewTakeoverRepository(db))
|
||||
}
|
||||
|
||||
func TestValidateTakeoverInspectionChecklistsAllowsEmptySections(t *testing.T) {
|
||||
if err := validateTakeoverInspectionChecklists(&requestdto.ReserveAcInspectionCreateAttributes{}); err != nil {
|
||||
t.Fatalf("expected empty sections to be allowed, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertTakeoverInspectionTxAllowsMissingFleetStatusFileOnUpdate(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
inspectionDate := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
if err := db.Create(&flightinspection.FlightInspection{
|
||||
ID: inspectionID,
|
||||
InspectionDate: inspectionDate,
|
||||
Status: flightinspection.StatusDraft,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create inspection: %v", err)
|
||||
}
|
||||
|
||||
req := &requestdto.ReserveAcInspectionCreateAttributes{
|
||||
InspectionDate: "2026-06-24",
|
||||
Before: requestdto.ReserveAcBeforeInspectionAttributes{
|
||||
OilEngineNR1Checked: boolPtr(true),
|
||||
OilEngineNR2Checked: boolPtr(true),
|
||||
HydraulicLHChecked: boolPtr(true),
|
||||
HydraulicRHChecked: boolPtr(true),
|
||||
OilTransmissionMGBChecked: boolPtr(true),
|
||||
OilTransmissionIGBChecked: boolPtr(false),
|
||||
OilTransmissionTGBChecked: boolPtr(true),
|
||||
FuelAmount: floatPtr(1),
|
||||
FuelUnit: stringPtr(beforeflightinspection.FuelUnitLT),
|
||||
FuelAddedAmount: floatPtr(0),
|
||||
},
|
||||
Prepare: requestdto.ReserveAcPrepareInspectionAttributes{
|
||||
NotamBriefing: boolPtr(true),
|
||||
WeatherBriefing: boolPtr(true),
|
||||
OperationalFlightPlan: boolPtr(false),
|
||||
},
|
||||
}
|
||||
|
||||
gotID, err := newChecklistTestService(db).upsertTakeoverInspectionTx(db, nil, inspectionID, req, helicopterID, uuidv7.MustBytes())
|
||||
if err != nil {
|
||||
t.Fatalf("expected update inspection without fleet_status_file to pass, got %v", err)
|
||||
}
|
||||
if string(gotID) != string(inspectionID) {
|
||||
t.Fatalf("unexpected inspection id: %x", gotID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertTakeoverInspectionTxAllowsMissingFileChecklistsOnUpdate(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
inspectionDate := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
if err := db.Create(&flightinspection.FlightInspection{
|
||||
ID: inspectionID,
|
||||
InspectionDate: inspectionDate,
|
||||
Status: flightinspection.StatusDraft,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create inspection: %v", err)
|
||||
}
|
||||
|
||||
req := &requestdto.ReserveAcInspectionCreateAttributes{
|
||||
InspectionDate: "2026-06-24",
|
||||
Before: requestdto.ReserveAcBeforeInspectionAttributes{
|
||||
OilEngineNR1Checked: boolPtr(true),
|
||||
OilEngineNR2Checked: boolPtr(true),
|
||||
HydraulicLHChecked: boolPtr(true),
|
||||
HydraulicRHChecked: boolPtr(true),
|
||||
OilTransmissionMGBChecked: boolPtr(true),
|
||||
OilTransmissionIGBChecked: boolPtr(false),
|
||||
OilTransmissionTGBChecked: boolPtr(true),
|
||||
FuelAmount: floatPtr(1),
|
||||
FuelUnit: stringPtr(beforeflightinspection.FuelUnitLT),
|
||||
FuelAddedAmount: floatPtr(0),
|
||||
},
|
||||
Prepare: requestdto.ReserveAcPrepareInspectionAttributes{
|
||||
NotamBriefing: boolPtr(true),
|
||||
WeatherBriefing: boolPtr(true),
|
||||
OperationalFlightPlan: boolPtr(false),
|
||||
},
|
||||
}
|
||||
|
||||
gotID, err := newChecklistTestService(db).upsertTakeoverInspectionTx(db, nil, inspectionID, req, helicopterID, uuidv7.MustBytes())
|
||||
if err != nil {
|
||||
t.Fatalf("expected update inspection without file_checklists to pass, got %v", err)
|
||||
}
|
||||
if string(gotID) != string(inspectionID) {
|
||||
t.Fatalf("unexpected inspection id: %x", gotID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHelicopterExistsTxDoesNotRequireDeletedAtColumn(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
|
||||
if err := db.Exec(`CREATE TABLE helicopters (
|
||||
id BLOB PRIMARY KEY
|
||||
)`).Error; err != nil {
|
||||
t.Fatalf("create helicopters table: %v", err)
|
||||
}
|
||||
if err := db.Exec(fmt.Sprintf(`INSERT INTO helicopters (id) VALUES (X'%x')`, helicopterID)).Error; err != nil {
|
||||
t.Fatalf("seed helicopter: %v", err)
|
||||
}
|
||||
|
||||
if err := ensureHelicopterExistsTx(db, helicopterID); err != nil {
|
||||
t.Fatalf("expected helicopter existence check to pass without deleted_at column, got %v", err)
|
||||
}
|
||||
if err := ensureHelicopterExistsTx(db, uuidv7.MustBytes()); err == nil {
|
||||
t.Fatalf("expected missing helicopter to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertTakeoverInspectionTxAllowsEmptyFileChecklistsOnUpdate(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
inspectionDate := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
if err := db.Create(&flightinspection.FlightInspection{
|
||||
ID: inspectionID,
|
||||
InspectionDate: inspectionDate,
|
||||
Status: flightinspection.StatusDraft,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create inspection: %v", err)
|
||||
}
|
||||
templateFile := helicopterfile.HelicopterFile{
|
||||
ID: uuidv7.MustBytes(),
|
||||
Section: helicopterfile.SectionBeforeFirstFlightInspection,
|
||||
Position: 1,
|
||||
IsMandatory: true,
|
||||
HelicopterID: helicopterID,
|
||||
FileAttachmentID: uuidv7.MustBytes(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(&templateFile).Error; err != nil {
|
||||
t.Fatalf("create helicopter file template: %v", err)
|
||||
}
|
||||
|
||||
req := &requestdto.ReserveAcInspectionCreateAttributes{
|
||||
InspectionDate: "2026-06-24",
|
||||
Before: requestdto.ReserveAcBeforeInspectionAttributes{
|
||||
OilEngineNR1Checked: boolPtr(true),
|
||||
OilEngineNR2Checked: boolPtr(true),
|
||||
HydraulicLHChecked: boolPtr(true),
|
||||
HydraulicRHChecked: boolPtr(true),
|
||||
OilTransmissionMGBChecked: boolPtr(true),
|
||||
OilTransmissionIGBChecked: boolPtr(false),
|
||||
OilTransmissionTGBChecked: boolPtr(true),
|
||||
FuelAmount: floatPtr(1),
|
||||
FuelUnit: stringPtr(beforeflightinspection.FuelUnitLT),
|
||||
FuelAddedAmount: floatPtr(0),
|
||||
FileChecklist: []requestdto.ReserveAcFileChecklistItem{},
|
||||
},
|
||||
Prepare: requestdto.ReserveAcPrepareInspectionAttributes{
|
||||
NotamBriefing: boolPtr(true),
|
||||
WeatherBriefing: boolPtr(true),
|
||||
OperationalFlightPlan: boolPtr(false),
|
||||
FileChecklist: []requestdto.ReserveAcFileChecklistItem{},
|
||||
},
|
||||
}
|
||||
|
||||
gotID, err := newChecklistTestService(db).upsertTakeoverInspectionTx(db, nil, inspectionID, req, helicopterID, uuidv7.MustBytes())
|
||||
if err != nil {
|
||||
t.Fatalf("expected update inspection with empty file_checklists to pass, got %v", err)
|
||||
}
|
||||
if string(gotID) != string(inspectionID) {
|
||||
t.Fatalf("unexpected inspection id: %x", gotID)
|
||||
}
|
||||
|
||||
var checklistCount int64
|
||||
if err := db.Table("flight_inspection_file_checklists").Count(&checklistCount).Error; err != nil {
|
||||
t.Fatalf("count file checklists: %v", err)
|
||||
}
|
||||
if checklistCount != 0 {
|
||||
t.Fatalf("expected no checklist rows to be created on empty update, got %d", checklistCount)
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool { return &v }
|
||||
|
||||
func floatPtr(v float64) *float64 { return &v }
|
||||
|
||||
func stringPtr(v string) *string { return &v }
|
||||
|
||||
func TestUpsertTakeoverChecklistItemsTxAllowsEmptyTemplateAndEmptyPayload(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
|
||||
err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", nil, uuidv7.MustBytes())
|
||||
if err != nil {
|
||||
t.Fatalf("expected empty template and empty payload to pass, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertTakeoverChecklistItemsTxRejectsItemsWhenTemplateMissing(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
fileID := uuidv7.MustBytes()
|
||||
fileIDStr, _ := uuidv7.BytesToString(fileID)
|
||||
|
||||
err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{{
|
||||
HelicopterFileID: fileIDStr,
|
||||
IsDone: true,
|
||||
}}, uuidv7.MustBytes())
|
||||
if err == nil {
|
||||
t.Fatal("expected error when template is missing and payload has items")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "inspection checklist template is empty for section fpwb") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertTakeoverChecklistItemsTxAllowsOptionalFalseButRequiresMandatoryTrue(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
now := time.Now().UTC()
|
||||
|
||||
mandatoryFile := helicopterfile.HelicopterFile{
|
||||
ID: uuidv7.MustBytes(),
|
||||
Section: "fpwb",
|
||||
Position: 1,
|
||||
IsMandatory: true,
|
||||
HelicopterID: helicopterID,
|
||||
FileAttachmentID: uuidv7.MustBytes(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
optionalFile := helicopterfile.HelicopterFile{
|
||||
ID: uuidv7.MustBytes(),
|
||||
Section: "fpwb",
|
||||
Position: 2,
|
||||
IsMandatory: false,
|
||||
HelicopterID: helicopterID,
|
||||
FileAttachmentID: uuidv7.MustBytes(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(&mandatoryFile).Error; err != nil {
|
||||
t.Fatalf("create mandatory file: %v", err)
|
||||
}
|
||||
if err := db.Create(&optionalFile).Error; err != nil {
|
||||
t.Fatalf("create optional file: %v", err)
|
||||
}
|
||||
|
||||
mandatoryIDStr, _ := uuidv7.BytesToString(mandatoryFile.ID)
|
||||
optionalIDStr, _ := uuidv7.BytesToString(optionalFile.ID)
|
||||
err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: mandatoryIDStr, IsDone: true},
|
||||
{HelicopterFileID: optionalIDStr, IsDone: false},
|
||||
}, uuidv7.MustBytes())
|
||||
if err != nil {
|
||||
t.Fatalf("expected optional false to pass, got %v", err)
|
||||
}
|
||||
|
||||
err = newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: mandatoryIDStr, IsDone: false},
|
||||
{HelicopterFileID: optionalIDStr, IsDone: false},
|
||||
}, uuidv7.MustBytes())
|
||||
if err == nil {
|
||||
t.Fatal("expected mandatory false to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "must be fully checked") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertTakeoverChecklistItemsTxAllowsPartialChecklist(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
now := time.Now().UTC()
|
||||
|
||||
mandatoryFile := helicopterfile.HelicopterFile{
|
||||
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 1, IsMandatory: true,
|
||||
HelicopterID: helicopterID, FileAttachmentID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
optionalFile := helicopterfile.HelicopterFile{
|
||||
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 2, IsMandatory: false,
|
||||
HelicopterID: helicopterID, FileAttachmentID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(&mandatoryFile).Error; err != nil {
|
||||
t.Fatalf("create mandatory file: %v", err)
|
||||
}
|
||||
if err := db.Create(&optionalFile).Error; err != nil {
|
||||
t.Fatalf("create optional file: %v", err)
|
||||
}
|
||||
|
||||
optionalIDStr, _ := uuidv7.BytesToString(optionalFile.ID)
|
||||
|
||||
// Submit ONLY the optional file; the unedited mandatory catalog file may be
|
||||
// omitted from the checklist (no more "must include all helicopter files").
|
||||
if err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: optionalIDStr, IsDone: true},
|
||||
}, uuidv7.MustBytes()); err != nil {
|
||||
t.Fatalf("expected partial checklist omitting non-edited files to pass, got %v", err)
|
||||
}
|
||||
|
||||
// Only the submitted item is recorded.
|
||||
var count int64
|
||||
if err := db.Table("flight_inspection_file_checklists").Where("flight_inspection_id = ?", inspectionID).Count(&count).Error; err != nil {
|
||||
t.Fatalf("count checklist rows: %v", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("expected exactly 1 checklist row, got %d", count)
|
||||
}
|
||||
|
||||
// A mandatory file that IS submitted but not done is still rejected.
|
||||
mandatoryIDStr, _ := uuidv7.BytesToString(mandatoryFile.ID)
|
||||
if err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: mandatoryIDStr, IsDone: false},
|
||||
}, uuidv7.MustBytes()); err == nil {
|
||||
t.Fatal("expected submitted-but-undone mandatory file to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertTakeoverChecklistItemsTxExcludesTemplateDerivedFiles(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
|
||||
t.Fatalf("migrate files: %v", err)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
inspectionID := uuidv7.MustBytes()
|
||||
|
||||
templateFileID := uuidv7.MustBytes()
|
||||
derivedFileID := uuidv7.MustBytes()
|
||||
folderID := uuidv7.MustBytes()
|
||||
if err := db.Create(&filemanager.File{ID: templateFileID, FolderID: folderID, Name: "template.docx", ObjectKey: "k1", IsTemplate: true}).Error; err != nil {
|
||||
t.Fatalf("create template file: %v", err)
|
||||
}
|
||||
// A document created from a template (has a non-empty template_uuid).
|
||||
if err := db.Create(&filemanager.File{ID: derivedFileID, FolderID: folderID, Name: "from-template.docx", ObjectKey: "k2", TemplateUUID: uuidv7.MustBytes()}).Error; err != nil {
|
||||
t.Fatalf("create derived file: %v", err)
|
||||
}
|
||||
|
||||
templateRow := helicopterfile.HelicopterFile{
|
||||
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 1,
|
||||
HelicopterID: helicopterID, SourceFileID: templateFileID, CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
derivedRow := helicopterfile.HelicopterFile{
|
||||
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 2,
|
||||
HelicopterID: helicopterID, SourceFileID: derivedFileID, CreatedAt: now, UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(&templateRow).Error; err != nil {
|
||||
t.Fatalf("create template row: %v", err)
|
||||
}
|
||||
if err := db.Create(&derivedRow).Error; err != nil {
|
||||
t.Fatalf("create derived row: %v", err)
|
||||
}
|
||||
|
||||
templateIDStr, _ := uuidv7.BytesToString(templateRow.ID)
|
||||
// Payload includes only the real template; the template-derived row must not be
|
||||
// required by the checklist completeness check.
|
||||
if err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: templateIDStr, IsDone: true},
|
||||
}, uuidv7.MustBytes()); err != nil {
|
||||
t.Fatalf("expected success when only the template is checked, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTakeoverEditedTemplateFilesTx(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
|
||||
t.Fatalf("migrate files: %v", err)
|
||||
}
|
||||
svc := newChecklistTestService(db)
|
||||
takeoverID := uuidv7.MustBytes()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
folderID := uuidv7.MustBytes()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// template_uuid on a from-template document == the source HelicopterFile.ID
|
||||
// that a checklist item's helicopter_file_id references.
|
||||
editedTemplateA := uuidv7.MustBytes()
|
||||
editedTemplateB := uuidv7.MustBytes()
|
||||
if err := db.Create(&helicopterfile.HelicopterFile{
|
||||
ID: editedTemplateA,
|
||||
Section: helicopterfile.SectionFlightPreparationAndWB,
|
||||
HelicopterID: helicopterID,
|
||||
SourceFileID: uuidv7.MustBytes(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create checklist template A: %v", err)
|
||||
}
|
||||
if err := db.Create(&helicopterfile.HelicopterFile{
|
||||
ID: editedTemplateB,
|
||||
Section: helicopterfile.SectionFlightPreparationAndWB,
|
||||
HelicopterID: helicopterID,
|
||||
SourceFileID: uuidv7.MustBytes(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create checklist template B: %v", err)
|
||||
}
|
||||
|
||||
// Two documents edited/created from a template for this takeover.
|
||||
if err := db.Create(&filemanager.File{ID: uuidv7.MustBytes(), FolderID: folderID, Name: "a.docx", ObjectKey: "a", TemplateUUID: editedTemplateA, TakeoverID: takeoverID}).Error; err != nil {
|
||||
t.Fatalf("create edited file A: %v", err)
|
||||
}
|
||||
if err := db.Create(&filemanager.File{ID: uuidv7.MustBytes(), FolderID: folderID, Name: "b.docx", ObjectKey: "b", TemplateUUID: editedTemplateB, TakeoverID: takeoverID}).Error; err != nil {
|
||||
t.Fatalf("create edited file B: %v", err)
|
||||
}
|
||||
// A from-template document for a different takeover must be ignored.
|
||||
if err := db.Create(&filemanager.File{ID: uuidv7.MustBytes(), FolderID: folderID, Name: "other.docx", ObjectKey: "o", TemplateUUID: uuidv7.MustBytes(), TakeoverID: uuidv7.MustBytes()}).Error; err != nil {
|
||||
t.Fatalf("create other-takeover file: %v", err)
|
||||
}
|
||||
|
||||
aStr, _ := uuidv7.BytesToString(editedTemplateA)
|
||||
bStr, _ := uuidv7.BytesToString(editedTemplateB)
|
||||
unrelatedStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
|
||||
|
||||
// 1) Checklist missing an edited file (B) -> reject.
|
||||
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: aStr, IsDone: true},
|
||||
}); err == nil {
|
||||
t.Fatalf("expected rejection when an edited file is missing from the checklist")
|
||||
}
|
||||
|
||||
// 2) Edited files exist but checklist is empty -> reject.
|
||||
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, nil); err == nil {
|
||||
t.Fatalf("expected rejection when edited files exist but checklist is empty")
|
||||
}
|
||||
|
||||
// 3) Checklist includes both edited files plus an unrelated (non-edited)
|
||||
// item -> success; a checklist item that was not edited is fine.
|
||||
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: aStr, IsDone: true},
|
||||
{HelicopterFileID: bStr, IsDone: true},
|
||||
{HelicopterFileID: unrelatedStr, IsDone: false},
|
||||
}); err != nil {
|
||||
t.Fatalf("expected success when all edited files are in the checklist, got %v", err)
|
||||
}
|
||||
|
||||
// 4) No edited files for the takeover -> success regardless of checklist.
|
||||
if err := svc.validateTakeoverEditedTemplateFilesTx(db, uuidv7.MustBytes(), uuidv7.MustBytes(), nil); err != nil {
|
||||
t.Fatalf("expected success when there are no edited files, got %v", err)
|
||||
}
|
||||
|
||||
// 5) A template-derived file that has already been trashed should not block
|
||||
// an inspection update when it is no longer part of the active takeover set.
|
||||
trashedTemplate := uuidv7.MustBytes()
|
||||
trashedTakeoverID := uuidv7.MustBytes()
|
||||
if err := db.Create(&filemanager.File{
|
||||
ID: uuidv7.MustBytes(),
|
||||
FolderID: folderID,
|
||||
Name: "trashed.docx",
|
||||
ObjectKey: "trashed",
|
||||
TemplateUUID: trashedTemplate,
|
||||
TakeoverID: trashedTakeoverID,
|
||||
Status: filemanager.FileStatusTrashed,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create trashed edited file: %v", err)
|
||||
}
|
||||
if err := svc.validateTakeoverEditedTemplateFilesTx(db, trashedTakeoverID, helicopterID, nil); err != nil {
|
||||
t.Fatalf("expected trashed edited file to be ignored, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTakeoverEditedTemplateFilesTxAcceptsLegacySourceFileReferences(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
|
||||
t.Fatalf("migrate files: %v", err)
|
||||
}
|
||||
svc := newChecklistTestService(db)
|
||||
takeoverID := uuidv7.MustBytes()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
folderID := uuidv7.MustBytes()
|
||||
now := time.Now().UTC()
|
||||
|
||||
sourceFileID := uuidv7.MustBytes()
|
||||
templateHeliFileID := uuidv7.MustBytes()
|
||||
if err := db.Create(&filemanager.File{
|
||||
ID: sourceFileID,
|
||||
FolderID: folderID,
|
||||
Name: "template.xlsx",
|
||||
ObjectKey: "template.xlsx",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create source template file: %v", err)
|
||||
}
|
||||
if err := db.Create(&helicopterfile.HelicopterFile{
|
||||
ID: templateHeliFileID,
|
||||
Section: helicopterfile.SectionFlightPreparationAndWB,
|
||||
HelicopterID: helicopterID,
|
||||
SourceFileID: sourceFileID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create helicopter template row: %v", err)
|
||||
}
|
||||
|
||||
// Legacy data may have template_uuid pointing to the source file ID instead
|
||||
// of the helicopter_file ID shown in the checklist payload.
|
||||
if err := db.Create(&filemanager.File{
|
||||
ID: uuidv7.MustBytes(),
|
||||
FolderID: folderID,
|
||||
Name: "draft.xlsx",
|
||||
ObjectKey: "draft.xlsx",
|
||||
TemplateUUID: sourceFileID,
|
||||
TakeoverID: takeoverID,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create legacy edited file: %v", err)
|
||||
}
|
||||
|
||||
templateChecklistID, _ := uuidv7.BytesToString(templateHeliFileID)
|
||||
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, []requestdto.ReserveAcFileChecklistItem{
|
||||
{HelicopterFileID: templateChecklistID, IsDone: true},
|
||||
}); err != nil {
|
||||
t.Fatalf("expected legacy source file template_uuid to map back to checklist template, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTakeoverEditedTemplateFilesTxIgnoresOrphanedTemplateReferences(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
|
||||
t.Fatalf("migrate files: %v", err)
|
||||
}
|
||||
svc := newChecklistTestService(db)
|
||||
takeoverID := uuidv7.MustBytes()
|
||||
helicopterID := uuidv7.MustBytes()
|
||||
folderID := uuidv7.MustBytes()
|
||||
|
||||
if err := db.Create(&filemanager.File{
|
||||
ID: uuidv7.MustBytes(),
|
||||
FolderID: folderID,
|
||||
Name: "orphaned-draft.xlsx",
|
||||
ObjectKey: "orphaned-draft.xlsx",
|
||||
TemplateUUID: uuidv7.MustBytes(),
|
||||
TakeoverID: takeoverID,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create orphaned edited file: %v", err)
|
||||
}
|
||||
|
||||
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, nil); err != nil {
|
||||
t.Fatalf("expected orphaned historical template reference to be ignored, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBaseShiftRangeTxPrefersOperationalShiftTime(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
category := &base.BaseCategory{ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now}
|
||||
if err := db.Create(category).Error; err != nil {
|
||||
t.Fatalf("create category: %v", err)
|
||||
}
|
||||
defaultStart := time.Date(1900, 1, 1, 9, 0, 0, 0, time.UTC)
|
||||
defaultEnd := time.Date(1900, 1, 1, 18, 0, 0, 0, time.UTC)
|
||||
baseID := uuidv7.MustBytes()
|
||||
row := &base.Base{
|
||||
ID: baseID,
|
||||
BaseCategoryID: category.ID,
|
||||
BaseName: "Base A",
|
||||
DefaultShiftStart: defaultStart.Format("15:04:05"),
|
||||
DefaultShiftEnd: defaultEnd.Format("15:04:05"),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
t.Fatalf("create base: %v", err)
|
||||
}
|
||||
opStart := time.Date(1900, 1, 1, 9, 0, 0, 0, time.UTC)
|
||||
opEnd := time.Date(1900, 1, 1, 17, 0, 0, 0, time.UTC)
|
||||
opDate := time.Date(2026, 4, 12, 0, 0, 0, 0, time.UTC)
|
||||
opRow := &base.BaseOperationalShiftTime{
|
||||
ID: uuidv7.MustBytes(),
|
||||
BaseID: baseID,
|
||||
DateStart: &opDate,
|
||||
DateEnd: &opDate,
|
||||
ShiftStart: opStart.Format("15:04:05"),
|
||||
ShiftEnd: opEnd.Format("15:04:05"),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(opRow).Error; err != nil {
|
||||
t.Fatalf("create operational shift: %v", err)
|
||||
}
|
||||
|
||||
start, end, err := ResolveBaseShiftRangeTx(db, baseID, time.Date(2026, 4, 12, 0, 0, 0, 0, time.UTC))
|
||||
if err != nil {
|
||||
t.Fatalf("resolve shift: %v", err)
|
||||
}
|
||||
if start != "09:00" || end != "17:00" {
|
||||
t.Fatalf("unexpected operational shift: start=%s end=%s", start, end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBaseShiftRangeTxFallsBackToDefaultShiftTime(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
category := &base.BaseCategory{ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now}
|
||||
if err := db.Create(category).Error; err != nil {
|
||||
t.Fatalf("create category: %v", err)
|
||||
}
|
||||
defaultStart := time.Date(1900, 1, 1, 9, 0, 0, 0, time.UTC)
|
||||
defaultEnd := time.Date(1900, 1, 1, 18, 0, 0, 0, time.UTC)
|
||||
baseID := uuidv7.MustBytes()
|
||||
row := &base.Base{
|
||||
ID: baseID,
|
||||
BaseCategoryID: category.ID,
|
||||
BaseName: "Base B",
|
||||
DefaultShiftStart: defaultStart.Format("15:04:05"),
|
||||
DefaultShiftEnd: defaultEnd.Format("15:04:05"),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
t.Fatalf("create base: %v", err)
|
||||
}
|
||||
|
||||
start, end, err := ResolveBaseShiftRangeTx(db, baseID, time.Date(2026, 4, 13, 0, 0, 0, 0, time.UTC))
|
||||
if err != nil {
|
||||
t.Fatalf("resolve shift: %v", err)
|
||||
}
|
||||
if start != "09:00" || end != "18:00" {
|
||||
t.Fatalf("unexpected default shift: start=%s end=%s", start, end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBaseShiftRangeTxResolvesBMCTECETFromCoordinates(t *testing.T) {
|
||||
db := openTakeoverServiceTestDB(t)
|
||||
now := time.Now().UTC()
|
||||
category := &base.BaseCategory{ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now}
|
||||
if err := db.Create(category).Error; err != nil {
|
||||
t.Fatalf("create category: %v", err)
|
||||
}
|
||||
baseID := uuidv7.MustBytes()
|
||||
row := &base.Base{
|
||||
ID: baseID,
|
||||
BaseCategoryID: category.ID,
|
||||
BaseName: "Base C",
|
||||
Latitude: 47.3769,
|
||||
Longitude: 8.5417,
|
||||
DefaultStartTimeType: base.ShiftTimeTypeBMCT,
|
||||
DefaultEndTimeType: base.ShiftTimeTypeECET,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
t.Fatalf("create base: %v", err)
|
||||
}
|
||||
|
||||
start, end, err := ResolveBaseShiftRangeTx(db, baseID, time.Date(2026, 4, 13, 0, 0, 0, 0, time.UTC))
|
||||
if err != nil {
|
||||
t.Fatalf("resolve shift: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(start) == "" || strings.TrimSpace(end) == "" {
|
||||
t.Fatalf("expected resolved BMCT/ECET shift, got start=%q end=%q", start, end)
|
||||
}
|
||||
if start == "09:00:00" || end == "18:00:00" {
|
||||
t.Fatalf("expected computed twilight times, got fixed-looking values start=%s end=%s", start, end)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user