From 09dfabb2b586b4d392a38611201cbeb1db1e01a9 Mon Sep 17 00:00:00 2001 From: Qingyuan Wang <162230229@nuaa.edu.cn> Date: Wed, 28 Oct 2026 02:22:16 +0000 Subject: [PATCH] fix(day): render unix timestamps in the time zone the caller asked for day.Format takes an IANA time zone name, loads it and then throws the result away: the returned string was always produced in the server local zone. Every caller passes the zone explicitly -- the email/HTML templates via the timeFormat helpers, and schema/user_schema.go even hardcodes "UTC" for the suspended-until notice -- so dates were shown shifted whenever the container zone differed from the configured one. Apply the loaded location with time.Time.In, and keep the previous local behaviour when the zone is empty or unknown so that a bad value cannot silently shift an instant. --- pkg/day/day.go | 9 +++++++-- pkg/day/day_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pkg/day/day.go b/pkg/day/day.go index 797da68b0..6c1cb498d 100644 --- a/pkg/day/day.go +++ b/pkg/day/day.go @@ -59,8 +59,13 @@ func Format(unix int64, format, tz string) (formatted string) { from = suffix } - _, _ = time.LoadLocation(tz) - formatted = time.Unix(unix, 0).Format(toFormat.String()) + loc, err := time.LoadLocation(tz) + if err != nil || tz == "" { + // An empty or unknown zone must not shift the instant that is being + // rendered: fall back to the server local zone. + loc = time.Local + } + formatted = time.Unix(unix, 0).In(loc).Format(toFormat.String()) return } diff --git a/pkg/day/day_test.go b/pkg/day/day_test.go index 1ff757664..4867421ac 100644 --- a/pkg/day/day_test.go +++ b/pkg/day/day_test.go @@ -102,3 +102,44 @@ func TestFormat_AllLanguagesNoHang(t *testing.T) { } } } + +// Format takes an IANA time zone name and must render the instant in that zone. +// Before the fix the loaded location was discarded, so every caller (the UI, +// the email templates and the "account suspended until" notice, which passes +// "UTC" explicitly) got the server's local zone instead. +func TestFormatTimezone(t *testing.T) { + // 2023-11-14 22:13:20 UTC + const sec = int64(1700000000) + + tests := []struct { + name string + format string + tz string + expected string + }{ + {"utc", "YYYY-MM-DD HH:mm", "UTC", "2023-11-14 22:13"}, + {"tokyo_crosses_midnight", "YYYY-MM-DD HH:mm", "Asia/Tokyo", "2023-11-15 07:13"}, + {"new_york", "YYYY-MM-DD HH:mm", "America/New_York", "2023-11-14 17:13"}, + {"kolkata_half_hour_offset", "YYYY-MM-DD HH:mm", "Asia/Kolkata", "2023-11-15 03:43"}, + {"los_angeles_long_date", "MMMM D, YYYY HH:mm", "America/Los_Angeles", "November 14, 2023 14:13"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, Format(sec, tt.format, tt.tz)) + }) + } +} + +// A zone that cannot be resolved must fall back to the server's local zone +// instead of failing or silently shifting the instant. +func TestFormatUnknownTimezoneFallsBackToLocal(t *testing.T) { + const sec = int64(1700000000) + expected := time.Unix(sec, 0).In(time.Local).Format("2006-01-02 15:04") + + for _, tz := range []string{"", "Not/AZone", "UTC+99"} { + t.Run("tz="+tz, func(t *testing.T) { + assert.Equal(t, expected, Format(sec, "YYYY-MM-DD HH:mm", tz)) + }) + } +}