Skip to content
quarry Artifact [efa6ef13d7]
Login

Artifact [efa6ef13d7]

Artifact efa6ef13d7c33f8832d0d4c050352ccfb337ac07cee6efe6b0e9239cf923d290:


package webhook

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/events"
	"craftdesign.group/quarry/internal/evt"
)

// The whole chain: something raises a vocabulary event and a third-party
// endpoint receives it, without the raiser knowing an endpoint exists.
//
// The raiser here is internal/events directly, which is what BOTH the
// fold and the ops layer call — so this proves the property the change
// was for: a delivery no longer depends on how the change arrived.
func TestVocabularyEventReachesAnEndpoint(t *testing.T) {
	bodies := make(chan []byte, 4)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		b, _ := io.ReadAll(r.Body)
		bodies <- b
		w.WriteHeader(http.StatusOK)
	}))
	defer srv.Close()

	ev, err := events.Start(filepath.Join(t.TempDir(), "nats"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	subs := &fakeSubs{subs: map[int64]Sub{
		1: {ID: 1, URL: srv.URL, Secret: "k", Active: true},
	}}
	d, err := New(ev.JS(), subs)
	if err != nil {
		t.Fatal(err)
	}
	d.Now = func() string { return "2026-08-24T00:00:00Z" }
	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	if err := d.Start(ctx); err != nil {
		t.Fatal(err)
	}
	if err := d.Fanout(ctx); err != nil {
		t.Fatal(err)
	}
	// The fan-out consumer starts at the tip, so a raise must not race
	// its subscription.
	time.Sleep(300 * time.Millisecond)

	if err := ev.Raise(evt.TicketClosed, "alpha", map[string]any{
		"ticket": "abcdef0123456789aa", "actor": "dan",
		"hash": "ffee0011223344556677", "date": "2026-08-24T10:00:00Z",
	}); err != nil {
		t.Fatal(err)
	}

	select {
	case body := <-bodies:
		var e Event
		if err := json.Unmarshal(body, &e); err != nil {
			t.Fatal(err)
		}
		if e.Kind != "ticket.closed" {
			t.Errorf("kind = %q, want ticket.closed — the subject carries it now", e.Kind)
		}
		if e.Repo != "alpha" || e.Actor != "dan" {
			t.Errorf("repo/actor = %q/%q", e.Repo, e.Actor)
		}
		if e.Link != "/r/alpha/ticket/abcdef0123456789" {
			t.Errorf("link = %q", e.Link)
		}
		if e.At != "2026-08-24T10:00:00Z" {
			t.Errorf("at = %q, want the event's own date", e.At)
		}
		if e.ID != "ticket.closed:ffee0011223344556677" {
			t.Errorf("id = %q; it must be stable across redeliveries", e.ID)
		}
	case <-time.After(20 * time.Second):
		t.Fatal("a raised event never reached the endpoint")
	}
}

func TestLinkAndSummaryPerNoun(t *testing.T) {
	for _, tc := range []struct {
		kind    string
		payload map[string]any
		link    string
		summary string
	}{
		{"checkin.created", map[string]any{"hash": "aabbccddeeff00112233", "summary": "fix it"},
			"/r/alpha/checkin/aabbccddeeff0011", "fix it"},
		{"branch.moved", map[string]any{"hash": "aabbccddeeff00112233", "branch": "feature"},
			"/r/alpha/checkin/aabbccddeeff0011", "branch feature moved"},
		{"wiki.created", map[string]any{"page": "Home"},
			"/r/alpha/wiki/Home", "wiki page Home created"},
		{"tag.created", map[string]any{"tag": "release", "target": "aabbccddeeff00112233"},
			"/r/alpha/checkin/aabbccddeeff0011", "tag release created"},
		{"landing.accepted", map[string]any{"landing": "1122334455667788aa"},
			"/r/alpha/landing/1122334455667788", "landing.accepted"},
		// A noun with no page gets no link rather than a broken one.
		{"clone.cloned", map[string]any{}, "", "clone.cloned"},
	} {
		e := eventOf(raised{Kind: tc.kind, Repo: "alpha", Payload: tc.payload}, "now")
		if e.Link != tc.link {
			t.Errorf("%s link = %q, want %q", tc.kind, e.Link, tc.link)
		}
		if e.Summary != tc.summary {
			t.Errorf("%s summary = %q, want %q", tc.kind, e.Summary, tc.summary)
		}
	}
}