Files
pangolin/server/internal/store/mysql_test.go
T
wangjia 52857d1d55 feat(1e): config PANGOLIN_ prefix + store.Open UTC DSN + go:embed migrations (tsk_zRA6fGU1JuHj)
- internal/config: rewrite Config with PANGOLIN_ prefix fields
  (HTTPAddr, AdminAddr, GRPCAddr, MySQL DSN/fields, RedisAddr,
  AutoMigrate, JWTSecret placeholder, WebhookSecret); Load() replaces
  FromEnv(); missing required MySQL vars return named-field error.

- internal/store/mysql.go: single Open(cfg) entry point; uses
  mysql.ParseDSN to structurally override ParseTime=true, Loc=UTC,
  Collation=utf8mb4_unicode_ci, Params[time_zone]='+00:00'; asserts
  SELECT @@session.time_zone=+00:00 after Ping (startup fatal).

- migrations/embed.go: //go:embed *.sql exposes var FS embed.FS.

- internal/store/migrate.go: MigrateUp/MigrateDown/MigrateVersion
  backed by golang-migrate iofs source + mysql driver; ErrNoChange
  treated as success.

- cmd/migrate/main.go: filled — up/down/version subcommands, reads
  config.Load() + store.Open.

- cmd/server/main.go: startup sequence Load → store.Open (UTC assert)
  → MigrateUp (if PANGOLIN_AUTO_MIGRATE=true) → HTTP listen;
  structured slog output at each step.

- internal/store/mysql_test.go: pure-function unit tests for buildDSN
  (empty-fields case + conflicting params overridden case); both pass.
- internal/store/mysql_integration_test.go: //go:build integration;
  testcontainers mysql:8 — UTC assertion + MigrateUp idempotency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 14:52:27 +08:00

76 lines
2.0 KiB
Go

package store
import (
"testing"
"time"
"github.com/go-sql-driver/mysql"
"github.com/wangjia/pangolin/server/internal/config"
)
// TestBuildDSN_IndividualFields verifies that individual config fields are
// assembled into a DSN with UTC parameters enforced.
func TestBuildDSN_IndividualFields(t *testing.T) {
cfg := config.Config{
MySQLHost: "localhost",
MySQLPort: "3306",
MySQLUser: "root",
MySQLPassword: "secret",
MySQLDBName: "pangolin",
}
dsn, err := buildDSN(cfg)
if err != nil {
t.Fatalf("buildDSN: %v", err)
}
parsed, err := mysql.ParseDSN(dsn)
if err != nil {
t.Fatalf("ParseDSN on output: %v", err)
}
if !parsed.ParseTime {
t.Error("ParseTime should be true")
}
if parsed.Loc != time.UTC {
t.Errorf("Loc = %v, want time.UTC", parsed.Loc)
}
if got := parsed.Params["time_zone"]; got != "'+00:00'" {
t.Errorf("time_zone param = %q, want \"'+00:00'\"", got)
}
if parsed.Collation != "utf8mb4_unicode_ci" {
t.Errorf("Collation = %q, want utf8mb4_unicode_ci", parsed.Collation)
}
}
// TestBuildDSN_ConflictingParamsOverridden verifies that a DSN that already
// contains conflicting timezone / locale settings is corrected by buildDSN.
func TestBuildDSN_ConflictingParamsOverridden(t *testing.T) {
// A DSN with parseTime=false, the wrong timezone, and a local location.
// buildDSN must override all of these.
cfg := config.Config{
MySQLDSN: "root:pass@tcp(localhost:3306)/db?parseTime=false&time_zone=%27Asia%2FShanghai%27",
}
dsn, err := buildDSN(cfg)
if err != nil {
t.Fatalf("buildDSN: %v", err)
}
parsed, err := mysql.ParseDSN(dsn)
if err != nil {
t.Fatalf("ParseDSN on output: %v", err)
}
if !parsed.ParseTime {
t.Error("ParseTime should be overridden to true")
}
if parsed.Loc != time.UTC {
t.Errorf("Loc = %v, want time.UTC (override of Local)", parsed.Loc)
}
if got := parsed.Params["time_zone"]; got != "'+00:00'" {
t.Errorf("time_zone param = %q, want \"'+00:00'\" (override)", got)
}
}