From c39df3043931dbe37d51a73d15f050a1c61d1a3b Mon Sep 17 00:00:00 2001 From: n30nex Date: Tue, 29 Sep 2026 18:37:14 -0400 Subject: [PATCH] fix(ingest): accept RFC3339 timestamp offsets and precision --- internal/ingest/packet.go | 13 +------ internal/ingest/timestamp_test.go | 64 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 internal/ingest/timestamp_test.go diff --git a/internal/ingest/packet.go b/internal/ingest/packet.go index 92f3da78..6890fe66 100644 --- a/internal/ingest/packet.go +++ b/internal/ingest/packet.go @@ -735,20 +735,11 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [ w.log.Error(fmt.Sprintf("db: upsert packet failed from %s/%s", iata, pubkeyHex), "error", err) return } - // Try parsing with timezone offset first - heardAt, err := time.Parse("2006-01-02T15:04:05.000000-07:00", envelope.Timestamp) - if err != nil { - heardAt, err = time.Parse("2006-01-02T15:04:05.000000", envelope.Timestamp) - } - if err != nil { - heardAt, err = time.Parse("2006-01-02T15:04:05.000000Z", envelope.Timestamp) - } + // Prefer an explicit RFC3339 offset; legacy timezone-less observers use UTC. + heardAt, err := time.Parse(time.RFC3339Nano, envelope.Timestamp) if err != nil { heardAt, err = time.Parse("2006-01-02T15:04:05", envelope.Timestamp) } - if err != nil { - heardAt, err = time.Parse("2006-01-02T15:04:05Z", envelope.Timestamp) - } if err != nil { w.log.Warn(fmt.Sprintf("failed to parse timestamp %q", envelope.Timestamp), "error", err) heardAt = time.Now().UTC() diff --git a/internal/ingest/timestamp_test.go b/internal/ingest/timestamp_test.go new file mode 100644 index 00000000..1b5daf66 --- /dev/null +++ b/internal/ingest/timestamp_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package ingest + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" +) + +func TestPacketTimestampFormatsAndSkew(t *testing.T) { + want := time.Now().UTC().Add(-10 * time.Minute).Truncate(time.Second).Add(123456789 * time.Nanosecond) + capture := func(t *testing.T, timestamp string) time.Time { + t.Helper() + w, base := newTestWorker() + db := &frameCaptureDB{stubDB: base} + w.db = db + var envelope map[string]string + if err := json.Unmarshal(packetEnvelope(t, buildGrpTxtPacket(t, 0x1a, make([]byte, 16))), &envelope); err != nil { + t.Fatal(err) + } + envelope["timestamp"] = timestamp + body, err := json.Marshal(envelope) + if err != nil { + t.Fatal(err) + } + w.handlePacket(context.Background(), "YOW", strings.Repeat("01", 32), body) + if len(db.observed) != 1 { + t.Fatal("timestamp prevented packet storage") + } + return db.observed[0].HeardAt + } + for _, zone := range []*time.Location{time.UTC, time.FixedZone("zero", 0), time.FixedZone("west", -4*60*60), time.FixedZone("east", 19800)} { + for _, precision := range []time.Duration{time.Second, time.Millisecond, time.Microsecond, time.Nanosecond} { + expected := want.Truncate(precision) + stamp := expected.In(zone).Format("2006-01-02T15:04:05.999999999-07:00") + if zone == time.UTC { + stamp = expected.Format(time.RFC3339Nano) + } + t.Run(stamp, func(t *testing.T) { + if got := capture(t, stamp); !got.Equal(expected) { + t.Fatalf("reported time changed: got %s want %s", got, expected) + } + }) + } + } + for _, precision := range []time.Duration{time.Second, time.Microsecond, time.Nanosecond} { + expected := want.Truncate(precision) + stamp := expected.Format("2006-01-02T15:04:05.999999999") + if got := capture(t, stamp); !got.Equal(expected) { + t.Fatalf("timezone-less UTC changed: got %s want %s", got, expected) + } + } + for _, stamp := range []string{"", "invalid", "2026-09-29T25:00:00+00:00", time.Now().Add(31 * time.Minute).Format(time.RFC3339), time.Now().Add(-31 * time.Minute).Format(time.RFC3339)} { + before := time.Now().UTC() + got := capture(t, stamp) + if got.Before(before) || got.After(time.Now().UTC()) { + t.Fatalf("invalid/skewed time escaped server-time guard: %s => %s", stamp, got) + } + } +}