Files
fm_be/internal/repository/mysql/helicopter_repo_test.go
2026-07-16 22:16:45 +07:00

588 lines
20 KiB
Go

package mysql
import (
"context"
"fmt"
"strings"
"testing"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
afterflightinspection "wucher/internal/domain/after_flight_inspection"
filemanager "wucher/internal/domain/file_manager"
"wucher/internal/domain/helicopter"
"wucher/internal/shared/pkg/uuidv7"
)
func intPtr(v int) *int {
return &v
}
func openHelicopterTestDB(t *testing.T) *gorm.DB {
t.Helper()
dsn := fmt.Sprintf("file:helicopter_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
NowFunc: func() time.Time { return time.Now().UTC() },
})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&filemanager.Folder{}, &filemanager.File{}, &filemanager.Attachment{}, &helicopter.Helicopter{}); err != nil {
t.Fatalf("auto migrate: %v", err)
}
return db
}
func TestNewHelicopterRepository(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
if repo == nil || repo.db == nil {
t.Fatalf("expected repository initialized")
}
if repo.hasUsersTable {
t.Fatalf("expected users table cache to be false in base helicopter test db")
}
}
func TestHelicopterRepositoryCachesUsersTablePresence(t *testing.T) {
db := openHelicopterTestDB(t)
if err := db.Exec(`CREATE TABLE users (
id BLOB PRIMARY KEY,
first_name TEXT,
last_name TEXT,
username TEXT,
email TEXT
)`).Error; err != nil {
t.Fatalf("create users table: %v", err)
}
repo := NewHelicopterRepository(db)
if !repo.hasUsersTable {
t.Fatalf("expected users table cache to be true")
}
creatorID := uuidv7.MustBytes()
updaterID := uuidv7.MustBytes()
if err := db.Table("users").Create([]map[string]any{
{
"id": creatorID,
"first_name": "Ada",
"last_name": "Lovelace",
"username": "ada",
"email": "ada@example.com",
},
{
"id": updaterID,
"first_name": "Grace",
"last_name": "Hopper",
"username": "grace",
"email": "grace@example.com",
},
}).Error; err != nil {
t.Fatalf("seed users: %v", err)
}
row := &helicopter.Helicopter{
Designation: "H145",
Identifier: "PK-AUDIT",
Type: "Twin",
CreatedBy: creatorID,
UpdatedBy: updaterID,
}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("seed helicopter: %v", err)
}
got, err := repo.GetByID(context.Background(), row.ID)
if err != nil {
t.Fatalf("get helicopter: %v", err)
}
if got == nil {
t.Fatalf("expected helicopter row")
}
if got.CreatedByName != "Ada Lovelace" {
t.Fatalf("unexpected created_by_name: %q", got.CreatedByName)
}
if got.UpdatedByName != "Grace Hopper" {
t.Fatalf("unexpected updated_by_name: %q", got.UpdatedByName)
}
}
func TestHelicopterRepositoryCreate(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
row := &helicopter.Helicopter{Designation: "H145", Identifier: "PK-ABC", Type: "Twin"}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(row.ID) == 0 {
t.Fatalf("expected id set")
}
inactive := &helicopter.Helicopter{Designation: "Inactive", Identifier: "PK-INACTIVE", Type: "Single", IsActive: false}
if err := repo.Create(context.Background(), inactive); err != nil {
t.Fatalf("create inactive: %v", err)
}
gotInactive, err := repo.GetByID(context.Background(), inactive.ID)
if err != nil {
t.Fatalf("get inactive: %v", err)
}
if gotInactive == nil || gotInactive.IsActive {
t.Fatalf("expected inactive helicopter persisted as false")
}
sqlDB, _ := db.DB()
_ = sqlDB.Close()
if err := repo.Create(context.Background(), &helicopter.Helicopter{Designation: "AfterClose", Identifier: "PK-CLOSE", Type: "Twin"}); err == nil {
t.Fatalf("expected error on closed DB")
}
}
func TestHelicopterRepositoryUpdate(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
row := &helicopter.Helicopter{Designation: "Old", Identifier: "PK-OLD", Type: "Single"}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("seed create: %v", err)
}
row.Designation = "New"
row.Type = "Twin"
if err := repo.Update(context.Background(), row); err != nil {
t.Fatalf("unexpected error: %v", err)
}
loaded, err := repo.GetByID(context.Background(), row.ID)
if err != nil || loaded == nil || loaded.Designation != "New" || loaded.Type != "Twin" {
t.Fatalf("expected updated row")
}
sqlDB, _ := db.DB()
_ = sqlDB.Close()
if err := repo.Update(context.Background(), row); err == nil {
t.Fatalf("expected error on closed DB")
}
}
func TestHelicopterRepositoryUpdate_ReplacesFotoAttachmentID(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
now := time.Now().UTC()
folder := &filemanager.Folder{
ID: uuidv7.MustBytes(),
Name: "photos",
NameNormalized: "photos",
Depth: 0,
PathCache: "/photos",
NameSlot: "live",
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(folder).Error; err != nil {
t.Fatalf("create folder: %v", err)
}
makeFile := func(name, key string) *filemanager.File {
return &filemanager.File{
ID: uuidv7.MustBytes(),
FolderID: folder.ID,
Name: name,
NameNormalized: strings.ToLower(name),
Extension: "webp",
SizeBytes: 1234,
MimeType: "image/webp",
Bucket: "bucket",
ObjectKey: key,
Status: filemanager.FileStatusReady,
NameSlot: "live",
CreatedAt: now,
UpdatedAt: now,
}
}
oldFile := makeFile("old.webp", "objects/old.webp")
newFile := makeFile("new.webp", "objects/new.webp")
if err := db.Create(oldFile).Error; err != nil {
t.Fatalf("create old file: %v", err)
}
if err := db.Create(newFile).Error; err != nil {
t.Fatalf("create new file: %v", err)
}
oldAttachment := &filemanager.Attachment{
ID: uuidv7.MustBytes(),
FileID: oldFile.ID,
RefType: "helicopter_photo",
RefID: "helicopter-1",
IsPrimary: true,
CreatedAt: now,
UpdatedAt: now,
}
newAttachment := &filemanager.Attachment{
ID: uuidv7.MustBytes(),
FileID: newFile.ID,
RefType: "helicopter_photo",
RefID: "helicopter-1",
IsPrimary: true,
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(oldAttachment).Error; err != nil {
t.Fatalf("create old attachment: %v", err)
}
if err := db.Create(newAttachment).Error; err != nil {
t.Fatalf("create new attachment: %v", err)
}
row := &helicopter.Helicopter{
ID: uuidv7.MustBytes(),
Designation: "Old",
Identifier: "PK-OLD-ATT",
Type: "Single",
FotoAttachmentID: oldAttachment.ID,
FotoAttachment: oldAttachment,
CreatedAt: now,
UpdatedAt: now,
}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("seed create helicopter: %v", err)
}
row.FotoAttachmentID = newAttachment.ID
row.FotoAttachment = oldAttachment
if err := repo.Update(context.Background(), row); err != nil {
t.Fatalf("update helicopter: %v", err)
}
loaded, err := repo.GetByID(context.Background(), row.ID)
if err != nil {
t.Fatalf("get by id after update: %v", err)
}
if loaded == nil {
t.Fatalf("expected updated row")
}
if string(loaded.FotoAttachmentID) != string(newAttachment.ID) {
t.Fatalf("expected foto_attachment_id replaced")
}
}
func TestHelicopterRepositoryDelete(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
row := &helicopter.Helicopter{Designation: "DeleteMe", Identifier: "PK-DEL", Type: "Twin"}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("seed create: %v", err)
}
if err := repo.Delete(context.Background(), row.ID); err != nil {
t.Fatalf("unexpected error: %v", err)
}
got, err := repo.GetByID(context.Background(), row.ID)
if err != nil {
t.Fatalf("get by id after delete: %v", err)
}
if got != nil {
t.Fatalf("expected row deleted")
}
}
func TestHelicopterRepositoryGetByID(t *testing.T) {
t.Run("found", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
row := &helicopter.Helicopter{Designation: "Found", Identifier: "PK-FND", Type: "Twin"}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("seed create: %v", err)
}
got, err := repo.GetByID(context.Background(), row.ID)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == nil || got.Identifier != "PK-FND" {
t.Fatalf("expected row found")
}
})
t.Run("not found", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
got, err := repo.GetByID(context.Background(), uuidv7.MustBytes())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != nil {
t.Fatalf("expected nil for not found")
}
})
t.Run("db error", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
sqlDB, _ := db.DB()
_ = sqlDB.Close()
if _, err := repo.GetByID(context.Background(), uuidv7.MustBytes()); err == nil {
t.Fatalf("expected error on closed DB")
}
})
}
func TestHelicopterRepositoryList(t *testing.T) {
t.Run("success without limit", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "H145", Identifier: "PK-A", Type: "Twin"})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "H125", Identifier: "PK-B", Type: "Single"})
rows, total, err := repo.List(context.Background(), "", nil, "", 0, 0, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if total != 2 || len(rows) != 2 {
t.Fatalf("expected 2 rows, total=%d len=%d", total, len(rows))
}
})
t.Run("success with filter sort and limit", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Main", Identifier: "PK-MAIN", Type: "Twin"})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Backup", Identifier: "PK-BACK", Type: "Single"})
rows, total, err := repo.List(context.Background(), "MAIN", nil, "identifier DESC", 1, 0, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if total != 1 || len(rows) != 1 || rows[0].Identifier != "PK-MAIN" {
t.Fatalf("unexpected list result")
}
})
t.Run("default order active sortkey first and inactive last", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Gamma", Identifier: "PK-G", Type: "Twin", IsActive: true})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Beta", Identifier: "PK-B0", Type: "Twin", SortKey: intPtr(0), IsActive: true})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Charlie", Identifier: "PK-C", Type: "Twin", SortKey: intPtr(2), IsActive: true})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Alpha", Identifier: "PK-A1", Type: "Twin", IsActive: true})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Alpha", Identifier: "PK-A2", Type: "Twin", SortKey: intPtr(1), IsActive: true})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Zulu", Identifier: "PK-Z", Type: "Twin", IsActive: false})
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Bravo", Identifier: "PK-B", Type: "Twin", IsActive: false, SortKey: intPtr(9)})
rows, total, err := repo.List(context.Background(), "", nil, "", 0, 0, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if total != 7 || len(rows) != 7 {
t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
}
gotOrder := []string{
rows[0].Designation + "|" + rows[0].Identifier,
rows[1].Designation + "|" + rows[1].Identifier,
rows[2].Designation + "|" + rows[2].Identifier,
rows[3].Designation + "|" + rows[3].Identifier,
rows[4].Designation + "|" + rows[4].Identifier,
rows[5].Designation + "|" + rows[5].Identifier,
rows[6].Designation + "|" + rows[6].Identifier,
}
wantOrder := []string{
"Beta|PK-B0",
"Alpha|PK-A2",
"Charlie|PK-C",
"Alpha|PK-A1",
"Gamma|PK-G",
"Bravo|PK-B",
"Zulu|PK-Z",
}
for i := range wantOrder {
if gotOrder[i] != wantOrder[i] {
t.Fatalf("unexpected default order: got=%v want=%v", gotOrder, wantOrder)
}
}
})
t.Run("status filter", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
// Minimal schema for the booked/available EXISTS subquery.
if err := db.Exec(`CREATE TABLE reserve_acs (id BLOB PRIMARY KEY, helicopter_id BLOB, inspection_id BLOB, deleted_at DATETIME)`).Error; err != nil {
t.Fatalf("create reserve_acs: %v", err)
}
if err := db.Exec(`CREATE TABLE takeover_acs (id BLOB PRIMARY KEY, reserve_ac_id BLOB, deleted_at DATETIME)`).Error; err != nil {
t.Fatalf("create takeover_acs: %v", err)
}
if err := db.Exec(`CREATE TABLE flights (id BLOB PRIMARY KEY, reserve_ac_id BLOB, takeover_ac_id BLOB, deleted_at DATETIME)`).Error; err != nil {
t.Fatalf("create flights: %v", err)
}
if err := db.AutoMigrate(&afterflightinspection.AfterFlightInspection{}); err != nil {
t.Fatalf("auto migrate after flight inspection: %v", err)
}
aog := &helicopter.Helicopter{Designation: "AOG", Identifier: "PK-AOG", Type: "Twin", AirOnGround: true}
booked := &helicopter.Helicopter{Designation: "Booked", Identifier: "PK-BKD", Type: "Twin"}
closed := &helicopter.Helicopter{Designation: "Closed", Identifier: "PK-CLS", Type: "Twin"}
available := &helicopter.Helicopter{Designation: "Avail", Identifier: "PK-AVL", Type: "Twin"}
for _, h := range []*helicopter.Helicopter{aog, booked, closed, available} {
if err := repo.Create(context.Background(), h); err != nil {
t.Fatalf("create helicopter: %v", err)
}
}
// Give "booked" an active (non-deleted) flight assignment.
raID := uuidv7.MustBytes()
takeoverID := uuidv7.MustBytes()
if err := db.Table("reserve_acs").Create(map[string]any{"id": raID, "helicopter_id": booked.ID}).Error; err != nil {
t.Fatalf("insert reserve_ac: %v", err)
}
if err := db.Table("takeover_acs").Create(map[string]any{"id": takeoverID, "reserve_ac_id": raID}).Error; err != nil {
t.Fatalf("insert takeover_ac: %v", err)
}
if err := db.Table("flights").Create(map[string]any{"id": uuidv7.MustBytes(), "takeover_ac_id": takeoverID}).Error; err != nil {
t.Fatalf("insert flight: %v", err)
}
closedRAID := uuidv7.MustBytes()
closedTakeoverID := uuidv7.MustBytes()
closedInspectionID := uuidv7.MustBytes()
if err := db.Table("reserve_acs").Create(map[string]any{"id": closedRAID, "helicopter_id": closed.ID, "inspection_id": closedInspectionID}).Error; err != nil {
t.Fatalf("insert closed reserve_ac: %v", err)
}
if err := db.Table("takeover_acs").Create(map[string]any{"id": closedTakeoverID, "reserve_ac_id": closedRAID}).Error; err != nil {
t.Fatalf("insert closed takeover_ac: %v", err)
}
if err := db.Table("flights").Create(map[string]any{"id": uuidv7.MustBytes(), "takeover_ac_id": closedTakeoverID}).Error; err != nil {
t.Fatalf("insert closed flight: %v", err)
}
if err := db.Create(&afterflightinspection.AfterFlightInspection{FlightInspectionID: closedInspectionID, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}).Error; err != nil {
t.Fatalf("insert after flight inspection: %v", err)
}
cases := []struct {
name string
statuses []string
groundedIDs [][]byte
want []string
}{
{name: "aog only", statuses: []string{"aog"}, want: []string{"PK-AOG"}},
{name: "booked only", statuses: []string{"booked"}, want: []string{"PK-BKD"}},
{name: "available only", statuses: []string{"available"}, want: []string{"PK-AVL", "PK-CLS"}},
{name: "available and booked", statuses: []string{"available", "booked"}, want: []string{"PK-AVL", "PK-BKD", "PK-CLS"}},
{name: "mcf matches nothing", statuses: []string{"mcf"}, want: []string{}},
{name: "no filter returns all", statuses: nil, want: []string{"PK-AOG", "PK-AVL", "PK-BKD", "PK-CLS"}},
{name: "complaint-grounded excluded from available", statuses: []string{"available"}, groundedIDs: [][]byte{available.ID}, want: []string{"PK-CLS"}},
{name: "complaint-grounded appears as aog", statuses: []string{"aog"}, groundedIDs: [][]byte{available.ID}, want: []string{"PK-AOG", "PK-AVL"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rows, total, err := repo.List(context.Background(), "", tc.statuses, "identifier ASC", 0, 0, tc.groundedIDs)
if err != nil {
t.Fatalf("list: %v", err)
}
if int(total) != len(tc.want) || len(rows) != len(tc.want) {
t.Fatalf("count mismatch: total=%d len=%d want=%d", total, len(rows), len(tc.want))
}
got := make([]string, len(rows))
for i := range rows {
got[i] = rows[i].Identifier
}
for i := range tc.want {
if got[i] != tc.want[i] {
t.Fatalf("unexpected rows: got=%v want=%v", got, tc.want)
}
}
})
}
})
t.Run("count error", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
if err := db.Migrator().DropTable(&helicopter.Helicopter{}); err != nil {
t.Fatalf("drop table: %v", err)
}
if _, _, err := repo.List(context.Background(), "", nil, "", 10, 0, nil); err == nil {
t.Fatalf("expected count error")
}
})
t.Run("find error", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
_ = repo.Create(context.Background(), &helicopter.Helicopter{Designation: "Main", Identifier: "PK-ERR", Type: "Twin"})
if _, _, err := repo.List(context.Background(), "", nil, "designation ASC, )", 10, 0, nil); err == nil {
t.Fatalf("expected find error from invalid sort")
}
})
}
func TestHelicopterRepositoryNextReportNumber(t *testing.T) {
t.Run("success", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
row := &helicopter.Helicopter{
Designation: "H145",
Identifier: " PK-ABC ",
Type: "Twin",
ReportSequence: 7,
}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("seed create: %v", err)
}
reportNumber, sequence, err := repo.NextReportNumber(context.Background(), row.ID)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if reportNumber != "PK-ABC-0008" || sequence != 8 {
t.Fatalf("unexpected report number/sequence: %q %d", reportNumber, sequence)
}
loaded, err := repo.GetByID(context.Background(), row.ID)
if err != nil || loaded == nil || loaded.ReportSequence != 8 {
t.Fatalf("expected report_sequence updated")
}
})
t.Run("identifier empty", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
row := &helicopter.Helicopter{
Designation: "H145",
Identifier: " ",
Type: "Twin",
}
if err := repo.Create(context.Background(), row); err != nil {
t.Fatalf("seed create: %v", err)
}
if _, _, err := repo.NextReportNumber(context.Background(), row.ID); err == nil {
t.Fatalf("expected identifier empty error")
}
})
t.Run("not found", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
if _, _, err := repo.NextReportNumber(context.Background(), uuidv7.MustBytes()); err == nil {
t.Fatalf("expected not found error")
}
})
t.Run("db error", func(t *testing.T) {
db := openHelicopterTestDB(t)
repo := NewHelicopterRepository(db)
sqlDB, _ := db.DB()
_ = sqlDB.Close()
if _, _, err := repo.NextReportNumber(context.Background(), uuidv7.MustBytes()); err == nil {
t.Fatalf("expected error on closed DB")
}
})
}