fix: store registry page timestamps in UTC

- GORM stamped created_at and updated_at in the process's local time,
  which the timestamp columns keep as the wall clock; east of UTC a page
  without a content date got a sitemap lastmod in the future
- Page.BeforeSave stamps both in UTC for the admin's and the API's saves,
  and the admin's address change writes updated_at in UTC
This commit is contained in:
Jakub Zych
2026-11-04 19:24:15 +01:00
parent 49cc241f58
commit dfc85bc805
3 changed files with 70 additions and 3 deletions

View File

@@ -7,6 +7,7 @@ import (
"strings"
"sync"
"testing"
"time"
"git.golem15.com/golem15/sm-sitemap-plugin/contract"
"git.golem15.com/golem15/summercms/modules/cooler"
@@ -32,6 +33,58 @@ func (e *testEnv) refusal(t *testing.T, method, path, token string, body map[str
}
}
// TestRegistryTimestampsUTC pins the registry page's created_at and
// updated_at in UTC on a machine east of UTC: the admin create, the
// management API's update and the admin's address change each store the
// UTC wall clock, so a page without a content date never gets a lastmod
// in the future.
func TestRegistryTimestampsUTC(t *testing.T) {
local := time.Local
time.Local = time.FixedZone("UTC+3", 3*60*60)
t.Cleanup(func() { time.Local = local })
env := bootEnv(t)
token := env.admin(t, true)
stamps := func(id string) (created, updated time.Time) {
t.Helper()
var row struct {
CreatedAt time.Time
UpdatedAt time.Time
}
if err := env.db.Raw(`SELECT created_at, updated_at FROM golem15_seo_pages WHERE id = ?`, id).Scan(&row).Error; err != nil {
t.Fatal(err)
}
return row.CreatedAt, row.UpdatedAt
}
within := func(what string, got, from, to time.Time) {
t.Helper()
if got.Before(from.Add(-time.Second)) || got.After(to.Add(time.Second)) {
t.Fatalf("%s = %s, want between %s and %s (UTC)", what, got.Format(time.RFC3339), from.Format(time.RFC3339), to.Format(time.RFC3339))
}
}
from := time.Now().UTC()
id := env.createRegistryPage(t, token, map[string]any{"url": map[string]any{"en": "/kontakt"}, "title": map[string]any{"en": "Kontakt"}})
created, updated := stamps(id)
within("created_at after the admin create", created, from, time.Now().UTC())
within("updated_at after the admin create", updated, from, time.Now().UTC())
from = time.Now().UTC()
env.expect(t, http.StatusOK, http.MethodPut, "/_seo/api/v1/pages/"+id, token, map[string]any{"title": "Kontakt z nami", "content_updated_at": ""})
_, updated = stamps(id)
within("updated_at after the API update", updated, from, time.Now().UTC())
reg, _ := env.app.Lookup[*signpost.Registry]()
rec, err := reg.Describe(t.Context(), "seo-page", id, "")
if err != nil || rec == nil || rec.LastMod == nil || rec.LastMod.After(time.Now().UTC().Add(time.Second)) {
t.Fatalf("describe = %+v err=%v, want a lastmod no later than now", rec, err)
}
from = time.Now().UTC()
env.expect(t, http.StatusOK, http.MethodPut, pagesAPI+"/"+id, token, map[string]any{"url": map[string]any{"en": "/kontakt-z-nami"}})
_, updated = stamps(id)
within("updated_at after the admin address change", updated, from, time.Now().UTC())
}
// TestRegistry covers the page registry (D-33, UI-SPEC C21): a page made
// in the Pages screen is a seo-page record with the SEO tab, resolves ok,
// is listed in the sitemap file of its kind with hreflang alternates, the