| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- // 时区固定验收(team-lead 口径):进程 Asia/Shanghai + DSN loc 一致 + JSON 恒 `+08:00`。
- //
- // 两条验收判据(缺一即视为未完成):
- // 1. 含 `time.Time` 的响应 JSON 匹配 `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+08:00$`;
- // 2. **改机器/容器时区也不变**——本文件用 `TZ=UTC` / `TZ=America/New_York` 复现"CI 容器/异地部署",
- // 断言偏移仍恒为 `+08:00`(这正是修复前会真实错 8 小时的场景)。
- package tz
- import (
- "encoding/json"
- "regexp"
- "strings"
- "testing"
- "time"
- )
- // rfc3339Plus8 契约要求的 JSON 时间形状(RFC3339 + 固定 +08:00 偏移)。
- var rfc3339Plus8 = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+08:00$`)
- // TestApplyFixesProcessZone Apply 后进程时区恒为 Asia/Shanghai,且不受机器 TZ 影响。
- //
- // 说明:`TZ` 只在进程启动时影响 `time.Local` 的初值,运行期改 `TZ` 不会自动生效 ——
- // 这里刻意先改 `TZ` 再 `Apply()`,模拟"容器把 TZ 设成 UTC 后进程启动/重设时区"的最坏情况。
- func TestApplyFixesProcessZone(t *testing.T) {
- for _, machineTZ := range []string{"UTC", "America/New_York", "Europe/Berlin"} {
- t.Run("TZ="+machineTZ, func(t *testing.T) {
- t.Setenv("TZ", machineTZ) // 模拟 CI 容器/异地机器
- original := time.Local
- t.Cleanup(func() { time.Local = original })
- loc := Apply()
- if got := time.Local.String(); got != LocationName {
- t.Fatalf("Apply 后 time.Local = %q,期望 %q(不能跟随机器 TZ=%s)", got, LocationName, machineTZ)
- }
- if loc.String() != LocationName {
- t.Fatalf("Apply 返回值 = %q,期望 %q", loc.String(), LocationName)
- }
- // 偏移必须是 +08:00(28800 秒)
- if _, offset := time.Now().In(loc).Zone(); offset != fixedZoneOffset {
- t.Fatalf("TZ=%s 下时区偏移 = %d 秒,期望 %d(+08:00)", machineTZ, offset, fixedZoneOffset)
- }
- })
- }
- }
- // TestApplyIdempotent 重复 Apply 无副作用(多处装配/多单测共享时安全)。
- func TestApplyIdempotent(t *testing.T) {
- original := time.Local
- t.Cleanup(func() { time.Local = original })
- first := Apply()
- second := Apply()
- if first.String() != second.String() || second.String() != LocationName {
- t.Fatalf("Apply 不幂等:%q / %q", first, second)
- }
- }
- // TestJSONOffsetAlwaysPlus8 验收判据①:含 time.Time 的 JSON 恒带 `+08:00`。
- //
- // 覆盖两条真实路径:
- // - `time.Now()`(未显式指定时区 → 取 time.Local,即 Apply 后的 Asia/Shanghai);
- // - 从 DB 读出的时间经 `In(tz.Location())` 归一后序列化(仓储/Handler 的推荐写法)。
- func TestJSONOffsetAlwaysPlus8(t *testing.T) {
- original := time.Local
- t.Cleanup(func() { time.Local = original })
- t.Setenv("TZ", "UTC")
- Apply()
- type payload struct {
- Now time.Time `json:"now"`
- FromDB time.Time `json:"fromDb"`
- Explicit time.Time `json:"explicit"`
- }
- // 模拟"DB 里存的是无时区 datetime(3),驱动按 loc=Asia/Shanghai 扫成 +08:00"
- dbTime := time.Date(2026, 3, 1, 12, 30, 45, 123_000_000, Location())
- raw, err := json.Marshal(payload{
- Now: time.Now(),
- FromDB: dbTime.In(Location()),
- Explicit: time.Date(2026, 12, 31, 23, 59, 59, 0, Location()),
- })
- if err != nil {
- t.Fatalf("序列化失败: %v", err)
- }
- var decoded map[string]string
- if err := json.Unmarshal(raw, &decoded); err != nil {
- t.Fatalf("反序列化失败: %v", err)
- }
- for _, field := range []string{"now", "fromDb", "explicit"} {
- value, ok := decoded[field]
- if !ok {
- t.Fatalf("JSON 缺少字段 %s: %s", field, raw)
- }
- if !rfc3339Plus8.MatchString(value) {
- t.Fatalf("字段 %s = %q,不匹配契约 ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?\\+08:00$(TZ=UTC 下必须仍是 +08:00)", field, value)
- }
- }
- // 边界日:跨年零点必须是本地日历日(UTC 会退到 12-31 15:59:59,账期错位)
- if got := decoded["explicit"]; got != "2026-12-31T23:59:59+08:00" {
- t.Fatalf("边界时间 = %q,期望 2026-12-31T23:59:59+08:00", got)
- }
- }
- // TestLocationNeverFallsBackToUTC 时区加载的纵深防御:Location 名恒为 Asia/Shanghai、偏移恒 +8h。
- //
- // 即使容器内缺 tzdata(极端裁剪镜像),loadLocation 也只会回退到**固定 +08:00 区**,
- // 绝不会回退 UTC(那会把账期整体错 8 小时)。
- func TestLocationNeverFallsBackToUTC(t *testing.T) {
- loc := Location()
- if loc.String() != LocationName {
- t.Fatalf("Location = %q,期望 %q(不得回退 UTC)", loc.String(), LocationName)
- }
- if _, offset := time.Date(2026, 6, 1, 0, 0, 0, 0, loc).Zone(); offset != fixedZoneOffset {
- t.Fatalf("偏移 = %d 秒,期望 %d(中国无夏令时,全年 +08:00)", offset, fixedZoneOffset)
- }
- }
- // TestNormalizeDSN DSN 时区收口:覆盖 loc=Local / 缺失 / 已正确三种输入。
- func TestNormalizeDSN(t *testing.T) {
- cases := []struct {
- name string
- in string
- want string
- }{
- {
- name: "loc=Local 必须被改写(随机器漂移的根因)",
- in: "dev:pass@tcp(127.0.0.1:3306)/yuansiwei_2026?charset=utf8mb4&parseTime=True&loc=Local",
- want: "dev:pass@tcp(127.0.0.1:3306)/yuansiwei_2026?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai",
- },
- {
- name: "loc=UTC 同样改写",
- in: "u:p@tcp(h:3306)/db?parseTime=False&loc=UTC",
- want: "u:p@tcp(h:3306)/db?parseTime=true&loc=Asia%2FShanghai",
- },
- {
- name: "无查询串 → 自动补 ? 与两个参数",
- in: "u:p@tcp(h:3306)/db",
- want: "u:p@tcp(h:3306)/db?parseTime=true&loc=Asia%2FShanghai",
- },
- {
- name: "已正确 → 幂等(其余参数与顺序不动)",
- in: "u:p@tcp(h:3306)/db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai&timeout=5s",
- want: "u:p@tcp(h:3306)/db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai&timeout=5s",
- },
- {
- name: "空 DSN 原样返回(由调用方 fail-fast)",
- in: " ",
- want: "",
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- got := NormalizeDSN(tc.in)
- if got != tc.want {
- t.Fatalf("NormalizeDSN(%q)\n got = %q\nwant = %q", tc.in, got, tc.want)
- }
- // 归一结果必须幂等(重复调用不再变化)
- if again := NormalizeDSN(got); again != got {
- t.Fatalf("NormalizeDSN 不幂等:%q → %q", got, again)
- }
- if got != "" && strings.Contains(got, "loc=Local") {
- t.Fatalf("归一后仍含 loc=Local:%q", got)
- }
- })
- }
- }
|