//go:build integration package store_test import ( "context" "testing" mysqlmodule "github.com/testcontainers/testcontainers-go/modules/mysql" "github.com/wangjia/pangolin/server/internal/config" "github.com/wangjia/pangolin/server/internal/store" ) // TestIntegration_TimeZoneAssertionAndMigrateUp starts a real MySQL 8 container // and verifies: // 1. store.Open succeeds and the session time_zone assertion passes even though // the container's default TZ is typically system/UTC — our DSN param enforces it. // 2. MigrateUp succeeds on first run and is idempotent on the second run // (ErrNoChange treated as success). // 3. MigrateDown succeeds and returns the schema to baseline. func TestIntegration_TimeZoneAssertionAndMigrateUp(t *testing.T) { ctx := context.Background() ctr, err := mysqlmodule.Run(ctx, "mysql:8", mysqlmodule.WithDatabase("pangolin"), mysqlmodule.WithUsername("root"), mysqlmodule.WithPassword("secret"), ) if err != nil { t.Fatalf("start mysql container: %v", err) } t.Cleanup(func() { if err := ctr.Terminate(ctx); err != nil { t.Logf("terminate container: %v", err) } }) // ConnectionString returns e.g. root:secret@tcp(localhost:PORT)/pangolin dsn, err := ctr.ConnectionString(ctx) if err != nil { t.Fatalf("connection string: %v", err) } cfg := &config.Config{DSN: dsn} // ── 1. Open (includes UTC assertion) ───────────────────────────────────── db, err := store.Open(cfg) if err != nil { t.Fatalf("store.Open: %v", err) } defer db.Close() // ── 2. First MigrateUp ─────────────────────────────────────────────────── if err := store.MigrateUp(db); err != nil { t.Fatalf("MigrateUp (first): %v", err) } v, dirty, err := store.MigrateVersion(db) if err != nil { t.Fatalf("MigrateVersion after up: %v", err) } if dirty { t.Errorf("schema is dirty after MigrateUp") } if v == 0 { t.Errorf("version is still 0 after MigrateUp — no migrations applied?") } t.Logf("after MigrateUp: version=%d dirty=%v", v, dirty) // ── 3. Idempotent second MigrateUp ─────────────────────────────────────── if err := store.MigrateUp(db); err != nil { t.Fatalf("MigrateUp (idempotent): %v", err) } // ── 4. MigrateDown ─────────────────────────────────────────────────────── if err := store.MigrateDown(db); err != nil { t.Fatalf("MigrateDown: %v", err) } v2, _, err := store.MigrateVersion(db) if err != nil { t.Fatalf("MigrateVersion after down: %v", err) } t.Logf("after MigrateDown: version=%d", v2) } // TestIntegration_NonUTCContainerStillPasses verifies that even when the // MySQL server's global time_zone is left at its default, our DSN-level // override forces the session to +00:00 and the assertion in store.Open passes. func TestIntegration_NonUTCContainerStillPasses(t *testing.T) { ctx := context.Background() ctr, err := mysqlmodule.Run(ctx, "mysql:8", mysqlmodule.WithDatabase("pangolin"), mysqlmodule.WithUsername("root"), mysqlmodule.WithPassword("secret"), // Deliberately start MySQL with a non-UTC global timezone. mysqlmodule.WithConfigFile(""), ) if err != nil { t.Fatalf("start mysql container: %v", err) } t.Cleanup(func() { _ = ctr.Terminate(ctx) }) dsn, err := ctr.ConnectionString(ctx) if err != nil { t.Fatalf("connection string: %v", err) } cfg := &config.Config{DSN: dsn} db, err := store.Open(cfg) if err != nil { t.Fatalf("store.Open with default container TZ: %v", err) } db.Close() }