Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/genesis-writer/entities_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (w *Writer) writeCommentPass(
EntityID: c.EntityID,
EntityType: c.EntityType,
TrackTimestampS: c.TrackTimestampS,
CreatedAt: c.CreatedAt.Format(time.RFC3339),
CreatedAt: c.CreatedAt.UTC().Format(time.RFC3339),
}

// Attach parent comment if this is a reply.
Expand Down Expand Up @@ -177,7 +177,7 @@ func (w *Writer) writeCommentReactions(ctx context.Context) error {
return cr, err
},
func(ctx context.Context, cr commentReaction) error {
metaJSON, err := json.Marshal(createdAtMeta{CreatedAt: cr.createdAt.Format(time.RFC3339)})
metaJSON, err := json.Marshal(createdAtMeta{CreatedAt: cr.createdAt.UTC().Format(time.RFC3339)})
if err != nil {
return fmt.Errorf("marshal comment reaction metadata: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/genesis-writer/entities_comment_pin.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (w *Writer) writeCommentPins(ctx context.Context) error {
func(ctx context.Context, p sourceCommentPin) error {
metaJSON, err := json.Marshal(commentPinMetadata{
EntityID: p.TrackID,
CreatedAt: p.PinnedAt.Format(time.RFC3339),
CreatedAt: p.PinnedAt.UTC().Format(time.RFC3339),
})
if err != nil {
return fmt.Errorf("marshal comment pin metadata for track %d: %w", p.TrackID, err)
Expand Down
2 changes: 1 addition & 1 deletion cmd/genesis-writer/entities_dashboard_wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (w *Writer) writeDashboardWalletUsers(ctx context.Context) error {
func(ctx context.Context, d sourceDashboardWalletUser) error {
metaJSON, err := json.Marshal(dashboardWalletMetadata{
Wallet: d.Wallet,
CreatedAt: d.CreatedAt.Format(time.RFC3339),
CreatedAt: d.CreatedAt.UTC().Format(time.RFC3339),
IsDelete: d.IsDelete,
})
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions cmd/genesis-writer/entities_developer_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (w *Writer) writeDeveloperApps(ctx context.Context) error {
Description: deref(d.Description),
ImageURL: deref(d.ImageURL),
IsPersonalAccess: d.IsPersonalAccess,
CreatedAt: d.CreatedAt.Format(time.RFC3339),
CreatedAt: d.CreatedAt.UTC().Format(time.RFC3339),
}
metaJSON, err := json.Marshal(meta)
if err != nil {
Expand Down Expand Up @@ -112,7 +112,7 @@ func (w *Writer) writeGrants(ctx context.Context) error {
metaJSON, err := json.Marshal(grantMetadata{
IsRevoked: g.IsRevoked,
GranteeAddress: g.GranteeAddress,
CreatedAt: g.CreatedAt.Format(time.RFC3339),
CreatedAt: g.CreatedAt.UTC().Format(time.RFC3339),
})
if err != nil {
return fmt.Errorf("marshal grant metadata: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions cmd/genesis-writer/entities_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ func (w *Writer) writeEvents(ctx context.Context) error {
meta := eventMetadata{
EventType: e.EventType,
EntityID: e.EntityID,
CreatedAt: e.CreatedAt.Format(time.RFC3339),
CreatedAt: e.CreatedAt.UTC().Format(time.RFC3339),
}
if e.EntityType != nil {
meta.EntityType = *e.EntityType
}
if e.EndDate != nil {
meta.EndDate = e.EndDate.Format(time.RFC3339)
meta.EndDate = e.EndDate.UTC().Format(time.RFC3339)
}
meta.EventData = unmarshalJSONB(e.EventData)

Expand Down
2 changes: 1 addition & 1 deletion cmd/genesis-writer/entities_playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (w *Writer) writePlaylists(ctx context.Context) error {
},
func(ctx context.Context, p sourcePlaylist) error {
inner := playlistMetadataInner{
CreatedAt: p.CreatedAt.Format(time.RFC3339),
CreatedAt: p.CreatedAt.UTC().Format(time.RFC3339),
PlaylistName: deref(p.PlaylistName),
Description: deref(p.Description),
IsAlbum: p.IsAlbum,
Expand Down
2 changes: 1 addition & 1 deletion cmd/genesis-writer/entities_social.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type socialMeta struct {
}

func fmtCreatedAt(t time.Time) string {
return t.Format(time.RFC3339)
return t.UTC().Format(time.RFC3339)
}

// --- Follows ---
Expand Down
4 changes: 2 additions & 2 deletions cmd/genesis-writer/entities_track.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ type sourceTrack struct {
// a column the writer forgets to carry is invisible in the output otherwise.
func buildTrackMetadata(t sourceTrack, collaborators []int64) trackMetadataInner {
inner := trackMetadataInner{
CreatedAt: t.CreatedAt.Format(time.RFC3339),
CreatedAt: t.CreatedAt.UTC().Format(time.RFC3339),
OwnerID: t.OwnerID,
Title: deref(t.Title),
Description: deref(t.Description),
Expand Down Expand Up @@ -352,7 +352,7 @@ func (w *Writer) writeTrackDownloads(ctx context.Context) error {
City: deref(d.City),
Region: deref(d.Region),
Country: deref(d.Country),
CreatedAt: d.CreatedAt.Format(time.RFC3339),
CreatedAt: d.CreatedAt.UTC().Format(time.RFC3339),
}
metaJSON, err := json.Marshal(meta)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/genesis-writer/entities_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (w *Writer) writeUsers(ctx context.Context) error {
},
func(ctx context.Context, u sourceUser) error {
meta := userMetadata{
CreatedAt: u.CreatedAt.Format(time.RFC3339),
CreatedAt: u.CreatedAt.UTC().Format(time.RFC3339),
Name: deref(u.Name),
Handle: deref(u.Handle),
Bio: deref(u.Bio),
Expand Down
103 changes: 103 additions & 0 deletions cmd/genesis-writer/timestamps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
)

// Every timestamp the writer emits must be rendered in UTC.
//
// The source schema mixes column types: most timestamps are `timestamp
// without time zone`, which pgx decodes with a UTC location, but seven columns
// the writer reads are `timestamp with time zone` -- playlist_tracks
// created_at/updated_at, email_access and encrypted_emails created_at/
// updated_at, and users.last_active_at. pgx decodes those into time.Local.
//
// Formatting a Local-zone time yields an offset-bearing string such as
// 2024-02-26T18:00:34-08:00 instead of 2024-02-27T02:00:34Z. Both name the
// same instant, and the indexer normalizes on read, so this is not currently
// data corruption -- but it makes the emitted bytes depend on the machine's
// TZ. The same source row would produce different metadata, and therefore a
// different transaction hash, on a UTC host than on a developer's laptop. A
// genesis artifact has to be reproducible.
//
// This is a source-level check on purpose. Most metadata is built inside
// per-entity closures that cannot be called in isolation, so a behavioural
// test would cover whichever one path it happened to reach. The failure mode
// worth guarding is a *new* emission site added later without .UTC(), and only
// scanning the package catches that.
func TestAllEmittedTimestampsAreUTC(t *testing.T) {
// A .Format(time.RFC3339) whose receiver does not end in .UTC().
unnormalized := regexp.MustCompile(`(\.UTC\(\))?\.Format\(time\.RFC3339\)`)

entries, err := os.ReadDir(".")
if err != nil {
t.Fatalf("read package dir: %v", err)
}

var offenders []string
checked := 0
for _, e := range entries {
name := e.Name()
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
src, err := os.ReadFile(filepath.Clean(name))
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
for i, line := range strings.Split(string(src), "\n") {
for _, m := range unnormalized.FindAllStringSubmatch(line, -1) {
checked++
if m[1] == "" { // no .UTC() before .Format
offenders = append(offenders,
filepath.Join(name)+":"+itoa(i+1)+" "+strings.TrimSpace(line))
}
}
}
}

if checked == 0 {
t.Fatal("found no RFC3339 formatting sites -- this guard has stopped guarding anything")
}
if len(offenders) > 0 {
t.Errorf("%d timestamp(s) emitted without .UTC(); on a non-UTC host these render "+
"with a local offset and the artifact stops being reproducible:\n %s",
len(offenders), strings.Join(offenders, "\n "))
}
}

// The premise above: a Local-zone time formats with an offset, and .UTC()
// removes it while naming the same instant. If the standard library ever
// stopped behaving this way the guard would be pointless.
func TestUTCNormalizationRemovesLocalOffset(t *testing.T) {
zone := time.FixedZone("PST", -8*60*60)
local := time.Date(2024, 2, 26, 18, 0, 34, 0, zone)

if got := local.Format(time.RFC3339); got != "2024-02-26T18:00:34-08:00" {
t.Fatalf("unnormalized = %s, want an offset-bearing timestamp", got)
}
got := local.UTC().Format(time.RFC3339)
if got != "2024-02-27T02:00:34Z" {
t.Errorf("normalized = %s, want 2024-02-27T02:00:34Z", got)
}
if !strings.HasSuffix(got, "Z") {
t.Errorf("normalized timestamp %s is not zone-independent", got)
}
}

func itoa(v int) string {
if v == 0 {
return "0"
}
var b []byte
for v > 0 {
b = append([]byte{byte('0' + v%10)}, b...)
v /= 10
}
return string(b)
}
Loading