package oltp
import (
"testing"
"time"
)
// "Did my hook fire?" is answered by recording what happened, not only
// what failed: a log holding only failures cannot say "it worked".
func TestDeliveriesRecordSuccessAndFailureAlike(t *testing.T) {
d := excDB(t)
for _, rec := range []Delivery{
{Sub: 1, Repo: "alpha", Event: "e1", Kind: "checkin", Status: 200, Ms: 42, Attempt: 1, Body: `{"id":"e1"}`},
{Sub: 1, Repo: "alpha", Event: "e2", Kind: "ticket", Status: 500, Ms: 91, Attempt: 2, Err: "endpoint answered 500"},
{Sub: 1, Repo: "alpha", Event: "e3", Kind: "checkin", Status: 0, Ms: 5000, Attempt: 3, Err: "dial tcp: timeout"},
{Sub: 2, Repo: "alpha", Event: "e4", Kind: "checkin", Status: 200, Ms: 7, Attempt: 1},
} {
d.RecordDelivery(rec)
}
got, err := d.Deliveries(1, 10)
if err != nil || len(got) != 3 {
t.Fatalf("%d rows err %v — one endpoint's attempts only", len(got), err)
}
// Newest first: the question is what happened LAST.
if got[0].Event != "e3" {
t.Errorf("order starts at %q", got[0].Event)
}
if !got[2].OK() || got[1].OK() || got[0].OK() {
t.Error("acceptance is not read from the status")
}
// A request that never completed is not a status of zero meaning
// success — it is the absence of an answer, and the error says so.
if got[0].Status != 0 || got[0].Err == "" {
t.Errorf("a failed connection recorded as %+v", got[0])
}
// The body is kept so a redelivery replays the exact bytes that
// produced the signature an endpoint rejected.
one, ok := d.DeliveryByID(got[2].ID)
if !ok || one.Body != `{"id":"e1"}` {
t.Errorf("the sent bytes were not kept: %+v", one)
}
// Another endpoint's attempts are its own.
if two, _ := d.Deliveries(2, 10); len(two) != 1 {
t.Errorf("endpoint 2 has %d attempts", len(two))
}
// An oversized body is clipped rather than refused: a delivery log
// that drops rows to protect itself defeats its own purpose.
big := make([]byte, deliveryBodyMax+500)
for i := range big {
big[i] = 'x'
}
d.RecordDelivery(Delivery{Sub: 3, Repo: "alpha", Event: "big", Kind: "k",
At: time.Now().UTC().Format(time.RFC3339Nano), Body: string(big)})
rows, _ := d.Deliveries(3, 10)
if len(rows) != 1 || len(rows[0].Body) != deliveryBodyMax {
t.Errorf("oversized body handling: %d rows", len(rows))
}
}
// Rotating a secret must not create a window in which every delivery is
// refused: the retired key keeps working for RotateWindow, and the
// dispatcher signs with both until it expires.
func TestRotateKeepsTheOldSecretForAWindow(t *testing.T) {
d := excDB(t)
w, err := d.AddWebhook("alpha", "https://x/y", "*")
if err != nil {
t.Fatal(err)
}
first := w.Secret
if first == "" {
t.Fatal("a new webhook has no secret")
}
next, err := d.RotateWebhookSecret(w.ID)
if err != nil {
t.Fatal(err)
}
if next == first {
t.Fatal("rotation returned the same key")
}
got, ok := d.WebhookByID(w.ID)
if !ok || got.Secret != next {
t.Fatalf("the new key is not in force: %+v", got)
}
if got.PreviousSecret() != first {
t.Errorf("the retired key is not honoured: %q", got.PreviousSecret())
}
// Past the window it stops, and it stops by being READ as expired
// rather than by a sweep — a forge that was down for the whole hour
// must not come back still accepting a key it promised to retire.
got.OldUntil = time.Now().Add(-time.Minute).UTC().Format(time.RFC3339)
if got.PreviousSecret() != "" {
t.Error("an expired key is still honoured")
}
// A second rotation retires the second key, not the first: an
// endpoint rotated twice in an hour must not accept the original.
third, err := d.RotateWebhookSecret(w.ID)
if err != nil {
t.Fatal(err)
}
after, _ := d.WebhookByID(w.ID)
if after.Secret != third || after.PreviousSecret() != next {
t.Errorf("a second rotation kept the wrong previous key: %q", after.PreviousSecret())
}
}