init push
This commit is contained in:
332
internal/repository/mysql/db_test.go
Normal file
332
internal/repository/mysql/db_test.go
Normal file
@@ -0,0 +1,332 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user