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

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")
}
})
}