333 lines
10 KiB
Go
333 lines
10 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"wucher/internal/config"
|
|
"wucher/internal/domain/auth"
|
|
"wucher/internal/domain/base"
|
|
facility "wucher/internal/domain/facility"
|
|
filemanager "wucher/internal/domain/file_manager"
|
|
flightdata "wucher/internal/domain/flight_data"
|
|
"wucher/internal/domain/helicopter"
|
|
"wucher/internal/domain/mission"
|
|
"wucher/internal/domain/transient"
|
|
)
|
|
|
|
func findMySQLDSNForTest() (string, bool) {
|
|
if v := strings.TrimSpace(os.Getenv("MYSQL_DSN")); v != "" {
|
|
return v, true
|
|
}
|
|
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
for i := 0; i < 8; i++ {
|
|
envPath := filepath.Join(dir, ".env")
|
|
raw, err := os.ReadFile(envPath)
|
|
if err == nil {
|
|
lines := strings.Split(string(raw), "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "MYSQL_DSN=") {
|
|
v := strings.TrimSpace(strings.TrimPrefix(line, "MYSQL_DSN="))
|
|
v = strings.Trim(v, `"'`)
|
|
if v != "" {
|
|
return v, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func openDBTestSQLite(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
dsn := fmt.Sprintf("file:db_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)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestConnect(t *testing.T) {
|
|
t.Run("missing dsn", func(t *testing.T) {
|
|
if _, err := Connect(config.MySQLConfig{}); err == nil {
|
|
t.Fatalf("expected missing dsn error")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid dsn", func(t *testing.T) {
|
|
_, err := Connect(config.MySQLConfig{
|
|
DSN: "root:pass@tcp(127.0.0.1:1)/wucher?loc=%",
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("expected connect error for invalid DSN")
|
|
}
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
dsn, ok := findMySQLDSNForTest()
|
|
if !ok {
|
|
t.Skip("MYSQL_DSN not found in env or .env")
|
|
}
|
|
|
|
cfg := config.MySQLConfig{
|
|
DSN: dsn,
|
|
MaxOpenConns: 7,
|
|
MaxIdleConns: 5,
|
|
ConnMaxLifetime: 2 * time.Minute,
|
|
ConnMaxIdleTime: time.Minute,
|
|
}
|
|
db, err := Connect(cfg)
|
|
if err != nil {
|
|
t.Skipf("skip connect success branch (mysql unavailable): %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("db.DB error: %v", err)
|
|
}
|
|
defer sqlDB.Close()
|
|
|
|
if err := sqlDB.PingContext(context.Background()); err != nil {
|
|
t.Fatalf("ping mysql: %v", err)
|
|
}
|
|
if got := sqlDB.Stats().MaxOpenConnections; got != cfg.MaxOpenConns {
|
|
t.Fatalf("unexpected max open conns, got=%d want=%d", got, cfg.MaxOpenConns)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAutoMigrate(t *testing.T) {
|
|
t.Run("success and base category backfill", func(t *testing.T) {
|
|
db := openDBTestSQLite(t)
|
|
|
|
if err := db.Exec(`CREATE TABLE helicopters (
|
|
id BLOB PRIMARY KEY,
|
|
designation TEXT,
|
|
identifier TEXT,
|
|
report_sequence INTEGER DEFAULT 0,
|
|
type TEXT,
|
|
counter INTEGER DEFAULT 0,
|
|
maintenance TEXT
|
|
)`).Error; err != nil {
|
|
t.Fatalf("create legacy helicopters table: %v", err)
|
|
}
|
|
if err := db.Exec(`CREATE TABLE bases (
|
|
id BLOB PRIMARY KEY,
|
|
base TEXT
|
|
)`).Error; err != nil {
|
|
t.Fatalf("create legacy bases table: %v", err)
|
|
}
|
|
if err := db.Exec(`INSERT INTO bases(id, base) VALUES (X'00112233445566778899aabbccddeeff', 'Legacy Base')`).Error; err != nil {
|
|
t.Fatalf("seed legacy bases row: %v", err)
|
|
}
|
|
if err := AutoMigrate(db); err != nil {
|
|
t.Fatalf("auto migrate: %v", err)
|
|
}
|
|
if db.Migrator().HasColumn(&helicopter.Helicopter{}, "maintenance") {
|
|
t.Fatalf("expected deprecated helicopters.maintenance dropped")
|
|
}
|
|
if db.Migrator().HasColumn(&helicopter.Helicopter{}, "counter") {
|
|
t.Fatalf("expected deprecated helicopters.counter dropped")
|
|
}
|
|
if !db.Migrator().HasColumn(&helicopter.Helicopter{}, "foto_attachment_id") {
|
|
t.Fatalf("expected helicopters.foto_attachment_id migrated")
|
|
}
|
|
if !db.Migrator().HasTable(&base.BaseCategory{}) {
|
|
t.Fatalf("expected base_categories table migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "base_category_id") {
|
|
t.Fatalf("expected bases.base_category_id migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "latitude") {
|
|
t.Fatalf("expected bases.latitude migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "longitude") {
|
|
t.Fatalf("expected bases.longitude migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "default_shift_start") {
|
|
t.Fatalf("expected bases.default_shift_start migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "default_shift_end") {
|
|
t.Fatalf("expected bases.default_shift_end migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "foto_attachment_id") {
|
|
t.Fatalf("expected bases.foto_attachment_id migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "deleted_at") {
|
|
t.Fatalf("expected bases.deleted_at migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.Base{}, "deleted_by") {
|
|
t.Fatalf("expected bases.deleted_by migrated")
|
|
}
|
|
if !db.Migrator().HasTable(&base.BaseOperationalShiftTime{}) {
|
|
t.Fatalf("expected base_operational_shift_times table migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.BaseOperationalShiftTime{}, "base_id") {
|
|
t.Fatalf("expected base_operational_shift_times.base_id migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.BaseOperationalShiftTime{}, "date_start") {
|
|
t.Fatalf("expected base_operational_shift_times.date_start migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.BaseOperationalShiftTime{}, "date_end") {
|
|
t.Fatalf("expected base_operational_shift_times.date_end migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.BaseOperationalShiftTime{}, "start_time_type") {
|
|
t.Fatalf("expected base_operational_shift_times.start_time_type migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&base.BaseOperationalShiftTime{}, "end_time_type") {
|
|
t.Fatalf("expected base_operational_shift_times.end_time_type migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&mission.Mission{}, "start_time") {
|
|
t.Fatalf("expected missions.start_time migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&mission.Mission{}, "end_time") {
|
|
t.Fatalf("expected missions.end_time migrated")
|
|
}
|
|
var missingCategory int64
|
|
if err := db.Table("bases").
|
|
Where("base_category_id IS NULL OR LENGTH(base_category_id) = 0").
|
|
Count(&missingCategory).Error; err != nil {
|
|
t.Fatalf("count missing base category: %v", err)
|
|
}
|
|
if missingCategory != 0 {
|
|
t.Fatalf("expected all bases to have category, missing=%d", missingCategory)
|
|
}
|
|
var seededCategories int64
|
|
if err := db.Table("base_categories").
|
|
Where("`key` IN (?, ?)", base.CategoryKeyRegular, base.CategoryKeyHEMS).
|
|
Count(&seededCategories).Error; err != nil {
|
|
t.Fatalf("count seeded categories: %v", err)
|
|
}
|
|
if seededCategories != 2 {
|
|
t.Fatalf("expected regular+hems seeded categories, got=%d", seededCategories)
|
|
}
|
|
if !db.Migrator().HasTable(&auth.User{}) {
|
|
t.Fatalf("expected core tables migrated")
|
|
}
|
|
if !db.Migrator().HasTable(&auth.UserRole{}) {
|
|
t.Fatalf("expected user_roles table migrated")
|
|
}
|
|
if !db.Migrator().HasTable(&transient.TokenEntry{}) || !db.Migrator().HasTable(&transient.QueueIdempotencyRecord{}) {
|
|
t.Fatalf("expected transient runtime tables migrated")
|
|
}
|
|
if !db.Migrator().HasTable(&filemanager.Folder{}) || !db.Migrator().HasTable(&filemanager.File{}) {
|
|
t.Fatalf("expected file manager tables migrated")
|
|
}
|
|
if !db.Migrator().HasColumn(&auth.Permission{}, "requires_pin") {
|
|
t.Fatalf("expected permissions.requires_pin migrated")
|
|
}
|
|
})
|
|
|
|
t.Run("facility category backfill", func(t *testing.T) {
|
|
db := openDBTestSQLite(t)
|
|
if err := db.AutoMigrate(&facility.Facility{}); err != nil {
|
|
t.Fatalf("auto migrate facilities: %v", err)
|
|
}
|
|
for _, category := range []string{
|
|
"fuel-truck",
|
|
"heslo-sling",
|
|
"logging-sling",
|
|
"hec-sling",
|
|
"hems-hec-sling",
|
|
} {
|
|
if err := db.Create(&facility.Facility{Category: category, Name: category, Type: "Sling"}).Error; err != nil {
|
|
t.Fatalf("seed facility category %q: %v", category, err)
|
|
}
|
|
}
|
|
if err := backfillFacilityCategories(db); err != nil {
|
|
t.Fatalf("backfillFacilityCategories: %v", err)
|
|
}
|
|
expected := []string{
|
|
"heslo-rope-label",
|
|
"heslo-rope-length",
|
|
"heslo-hook",
|
|
"hec-rope-label",
|
|
"hec-rope-length",
|
|
}
|
|
for _, category := range expected {
|
|
var count int64
|
|
if err := db.Table("facilities").Where("category = ?", category).Count(&count).Error; err != nil {
|
|
t.Fatalf("count category %q: %v", category, err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("expected one row for category %q, got %d", category, count)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("auto migrate error when db closed", func(t *testing.T) {
|
|
db := openDBTestSQLite(t)
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("db.DB: %v", err)
|
|
}
|
|
_ = sqlDB.Close()
|
|
|
|
if err := AutoMigrate(db); err == nil {
|
|
t.Fatalf("expected auto migrate error on closed DB")
|
|
}
|
|
})
|
|
|
|
t.Run("drop deprecated maintenance from legacy composite key schema", func(t *testing.T) {
|
|
db := openDBTestSQLite(t)
|
|
|
|
// Keep maintenance as part of PK to force drop-column failure on SQLite.
|
|
if err := db.Exec(`CREATE TABLE helicopters (
|
|
id BLOB NOT NULL,
|
|
maintenance TEXT NOT NULL,
|
|
designation TEXT,
|
|
identifier TEXT,
|
|
type TEXT,
|
|
report_sequence INTEGER DEFAULT 0,
|
|
PRIMARY KEY (id, maintenance)
|
|
)`).Error; err != nil {
|
|
t.Fatalf("create legacy helicopters table: %v", err)
|
|
}
|
|
|
|
if err := AutoMigrate(db); err != nil {
|
|
t.Fatalf("unexpected auto migrate error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ensure flight data status column on legacy table", func(t *testing.T) {
|
|
db := openDBTestSQLite(t)
|
|
if err := db.Exec(`CREATE TABLE flight_data (
|
|
id BLOB PRIMARY KEY
|
|
)`).Error; err != nil {
|
|
t.Fatalf("create legacy flight_data table: %v", err)
|
|
}
|
|
if err := ensureFlightDataStatusColumn(db); err != nil {
|
|
t.Fatalf("ensure status column: %v", err)
|
|
}
|
|
if !db.Migrator().HasColumn(&flightdata.FlightData{}, "status") {
|
|
t.Fatal("expected flight_data.status column to exist")
|
|
}
|
|
})
|
|
|
|
}
|