Skip to content
quarry Artifact [477d3d6eec]
Login

Artifact [477d3d6eec]

Artifact 477d3d6eec56ab30a6f4b4571db8a6cc30e0ff04468d044ed0f535d3ef120224:


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())
	}
}