Skip to content

Commit fa3b471

Browse files
authored
Merge pull request #495 from hookdeck/delivery-timestamp
fix: delivery timestamp metadata in unix seconds
2 parents 4beab5c + e667dca commit fa3b471

File tree

9 files changed

+78
-16
lines changed

9 files changed

+78
-16
lines changed

internal/destregistry/basepublisher.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func (p *BasePublisher) StartClose() {
3737

3838
func (p *BasePublisher) MakeMetadata(event *models.Event, timestamp time.Time) map[string]string {
3939
systemMetadata := map[string]string{
40-
"timestamp": fmt.Sprintf("%d", timestamp.UnixMilli()),
40+
"timestamp": fmt.Sprintf("%d", timestamp.Unix()),
4141
"event-id": event.ID,
4242
"topic": event.Topic,
4343
}

internal/destregistry/providers/destawskinesis/destawskinesis_publish_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ func (a *KinesisAsserter) AssertMessage(t testsuite.TestingT, msg testsuite.Mess
204204

205205
// Verify system metadata
206206
assert.NotEmpty(t, metadata["timestamp"], "timestamp should be present")
207+
testsuite.AssertTimestampIsUnixSeconds(t, metadata["timestamp"])
207208
assert.Equal(t, event.ID, metadata["event-id"], "event-id should match")
208209
assert.Equal(t, event.Topic, metadata["topic"], "topic should match")
209210

internal/destregistry/providers/destawss3/destawss3_publish_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func (a *S3Asserter) AssertMessage(t testsuite.TestingT, msg testsuite.Message,
140140
// 2. Assert system metadata is present
141141
metadata := msg.Metadata
142142
assert.NotEmpty(t, metadata["timestamp"], "timestamp should be present")
143+
testsuite.AssertTimestampIsUnixSeconds(t, metadata["timestamp"])
143144
assert.Equal(t, event.ID, metadata["event-id"], "event-id should match")
144145
assert.Equal(t, event.Topic, metadata["topic"], "topic should match")
145146

internal/destregistry/providers/destawssqs/destawssqs_publish_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func (a *SQSAsserter) AssertMessage(t testsuite.TestingT, msg testsuite.Message,
9494

9595
// Verify system metadata
9696
assert.NotEmpty(t, metadata["timestamp"], "timestamp should be present")
97+
testsuite.AssertTimestampIsUnixSeconds(t, metadata["timestamp"])
9798
assert.Equal(t, event.ID, metadata["event-id"], "event-id should match")
9899
assert.Equal(t, event.Topic, metadata["topic"], "topic should match")
99100

internal/destregistry/providers/destazureservicebus/destazureservicebus_publish_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ func (a *AzureServiceBusAsserter) AssertMessage(t testsuite.TestingT, msg testsu
119119

120120
// Verify system metadata
121121
assert.NotEmpty(t, metadata["timestamp"], "timestamp should be present")
122+
testsuite.AssertTimestampIsUnixSeconds(t, metadata["timestamp"])
122123
assert.Equal(t, event.ID, metadata["event-id"], "event-id should match")
123124
assert.Equal(t, event.Topic, metadata["topic"], "topic should match")
124125

internal/destregistry/providers/destrabbitmq/destrabbitmq_publish_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ func (a *RabbitMQAsserter) AssertMessage(t testsuite.TestingT, msg testsuite.Mes
147147
// Verify system metadata
148148
metadata := msg.Metadata
149149
assert.NotEmpty(t, metadata["timestamp"], "timestamp should be present")
150+
testsuite.AssertTimestampIsUnixSeconds(t, metadata["timestamp"])
150151
assert.Equal(t, event.ID, metadata["event-id"], "event-id should match")
151152
assert.Equal(t, event.Topic, metadata["topic"], "topic should match")
152153

internal/destregistry/providers/destwebhook/destwebhook.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -557,16 +557,31 @@ func (p *WebhookPublisher) Format(ctx context.Context, event *models.Event) (*ht
557557

558558
req.Header.Set("Content-Type", "application/json")
559559

560-
// Add default headers unless disabled
561-
if !p.disableTimestampHeader {
562-
req.Header.Set(p.headerPrefix+"timestamp", fmt.Sprintf("%d", now.UnixMilli()))
563-
}
564-
if !p.disableEventIDHeader {
565-
req.Header.Set(p.headerPrefix+"event-id", event.ID)
566-
}
567-
if !p.disableTopicHeader {
568-
req.Header.Set(p.headerPrefix+"topic", event.Topic)
560+
// Get merged metadata (system + event metadata) using BasePublisher
561+
metadata := p.BasePublisher.MakeMetadata(event, now)
562+
563+
// Add headers from metadata, respecting disable flags
564+
for key, value := range metadata {
565+
// Check if this specific system header should be disabled
566+
switch key {
567+
case "timestamp":
568+
if p.disableTimestampHeader {
569+
continue
570+
}
571+
case "event-id":
572+
if p.disableEventIDHeader {
573+
continue
574+
}
575+
case "topic":
576+
if p.disableTopicHeader {
577+
continue
578+
}
579+
}
580+
// Add the header with the appropriate prefix
581+
req.Header.Set(p.headerPrefix+key, value)
569582
}
583+
584+
// Add signature header if not disabled
570585
if !p.disableSignatureHeader {
571586
signatureHeader := p.sm.GenerateSignatureHeader(SignaturePayload{
572587
EventID: event.ID,
@@ -579,11 +594,6 @@ func (p *WebhookPublisher) Format(ctx context.Context, event *models.Event) (*ht
579594
}
580595
}
581596

582-
// Add metadata headers with the specified prefix
583-
for key, value := range event.Metadata {
584-
req.Header.Set(p.headerPrefix+strings.ToLower(key), value)
585-
}
586-
587597
return req, nil
588598
}
589599

internal/destregistry/providers/destwebhook/destwebhook_publish_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,12 @@ func (a *WebhookAsserter) AssertMessage(t testsuite.TestingT, msg testsuite.Mess
9191
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
9292

9393
// Verify default headers
94-
assert.NotEmpty(t, req.Header.Get(a.headerPrefix+"timestamp"), "timestamp header should be present")
94+
timestampHeader := req.Header.Get(a.headerPrefix + "timestamp")
95+
assert.NotEmpty(t, timestampHeader, "timestamp header should be present")
96+
97+
// Verify timestamp is in Unix seconds (not milliseconds)
98+
testsuite.AssertTimestampIsUnixSeconds(t, timestampHeader)
99+
95100
assert.Equal(t, event.ID, req.Header.Get(a.headerPrefix+"event-id"), "event-id header should match")
96101
assert.Equal(t, event.Topic, req.Header.Get(a.headerPrefix+"topic"), "topic header should match")
97102

@@ -105,6 +110,13 @@ func (a *WebhookAsserter) AssertMessage(t testsuite.TestingT, msg testsuite.Mess
105110
signatureHeader := req.Header.Get(a.headerPrefix + "signature")
106111
assertSignatureFormat(t, signatureHeader, a.expectedSignatures)
107112

113+
// Verify timestamp in signature header matches the timestamp header
114+
signatureParts := strings.SplitN(signatureHeader, ",", 2)
115+
if len(signatureParts) >= 2 {
116+
signatureTimestampStr := strings.TrimPrefix(signatureParts[0], "t=")
117+
assert.Equal(t, timestampHeader, signatureTimestampStr, "timestamp in signature header should match timestamp header")
118+
}
119+
108120
// Verify each expected signature
109121
for _, secret := range a.secrets {
110122
assertValidSignature(t, secret, msg.Data, signatureHeader)

internal/destregistry/testing/publisher_suite.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7+
"strconv"
78
"sync"
89
"sync/atomic"
910
"time"
1011

1112
"github.com/hookdeck/outpost/internal/destregistry"
1213
"github.com/hookdeck/outpost/internal/models"
1314
"github.com/hookdeck/outpost/internal/util/testutil"
15+
"github.com/stretchr/testify/assert"
1416
"github.com/stretchr/testify/suite"
1517
)
1618

@@ -35,6 +37,39 @@ type TestingT interface {
3537
Helper()
3638
}
3739

40+
// AssertTimestampIsUnixSeconds verifies that a timestamp string is in Unix seconds format (not milliseconds).
41+
// It checks if the timestamp is within a reasonable range for Unix seconds (between year 2000 and 2100).
42+
func AssertTimestampIsUnixSeconds(t TestingT, timestampStr string, msgAndArgs ...interface{}) {
43+
t.Helper()
44+
45+
timestampInt, err := strconv.ParseInt(timestampStr, 10, 64)
46+
assert.NoError(t, err, "timestamp should be a valid integer")
47+
48+
// Check if timestamp is in a reasonable range for Unix seconds
49+
// Year 2000: ~946,684,800
50+
// Year 2100: ~4,102,444,800
51+
// Current time in seconds: ~1,700,000,000 (2023-2024)
52+
// Current time in millis: ~1,700,000,000,000
53+
54+
minUnixSeconds := int64(946684800) // Jan 1, 2000
55+
maxUnixSeconds := int64(4102444800) // Jan 1, 2100
56+
57+
if timestampInt < minUnixSeconds || timestampInt > maxUnixSeconds {
58+
// Likely milliseconds - check if dividing by 1000 gives a reasonable timestamp
59+
possibleSeconds := timestampInt / 1000
60+
if possibleSeconds >= minUnixSeconds && possibleSeconds <= maxUnixSeconds {
61+
assert.Fail(t, "timestamp appears to be in milliseconds, expected Unix seconds",
62+
"timestamp %d is likely in milliseconds (would be %s if converted to seconds), expected Unix seconds (around %s)",
63+
timestampInt,
64+
time.Unix(possibleSeconds, 0).Format(time.RFC3339),
65+
time.Now().Format(time.RFC3339))
66+
} else {
67+
assert.Fail(t, "timestamp is out of reasonable range",
68+
"timestamp %d is not within reasonable Unix seconds range (year 2000-2100)", timestampInt)
69+
}
70+
}
71+
}
72+
3873
// MessageConsumer is the interface that providers must implement
3974
type MessageConsumer interface {
4075
// Consume returns a channel that receives messages

0 commit comments

Comments
 (0)