Skip to content
quarry Artifact [6ca768babd]
Login

Artifact [6ca768babd]

Artifact 6ca768babd012c8aab4cdb2224e8b20cb3da8cc07055be68b00080db64271eaa:


package web

import (
	"net/http/httptest"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/ops"
)

type fleetHooks struct{ rows []FleetHook }

func (fleetHooks) Webhooks(string) ([]HookSub, error)         { return nil, nil }
func (fleetHooks) AddWebhook(_, _, _ string) (HookSub, error) { return HookSub{}, nil }
func (fleetHooks) SetWebhookEvents(int64, string) error       { return nil }
func (fleetHooks) SetWebhookActive(int64, bool) error         { return nil }
func (fleetHooks) DeleteWebhook(int64) error                  { return nil }
func (f fleetHooks) FailingWebhooks() ([]FleetHook, error)    { return f.rows, nil }

func hookFleetForge(t *testing.T, h Hooks) *httptest.Server {
	t.Helper()
	accts := newBetaAccts()
	_ = accts.AddAccount("root", "root-pw", true)
	sess := &betaSessions{accts: accts, admin: "root"}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v", Accounts: accts,
		Repos: api.NewRepos(map[string]*api.Repo{})}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(sess), WithCaps(betaCaps{admin: "root"}), WithHooks(h)},
		Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })))
	t.Cleanup(srv.Close)
	return srv
}

// Exceptions lists, for site admins, every paused or failing webhook
// endpoint with the repository it belongs to, linking each to that
// repository's delivery log.
func TestFleetWebhooksListsFailingEndpoints(t *testing.T) {
	srv := hookFleetForge(t, fleetHooks{rows: []FleetHook{
		{ID: 7, Repo: "gamma", URL: "https://dead.test/h", PausedAt: "2026-11-04T10:00:00Z",
			PausedWhy: "5 deliveries in a row failed", FailStreak: 5, LastErr: "endpoint answered 503"},
		{ID: 9, Repo: "beta", URL: "https://flaky.test/h", Active: true, FailStreak: 2, LastErr: "timeout"},
		{ID: 11, Repo: "alpha", URL: "https://held.test/h"},
	}})
	if res := as(t, srv, "", "GET", "/fleet/exceptions", nil); res.StatusCode == 200 {
		t.Fatal("anonymous reached the fleet exceptions view")
	}
	page := body(as(t, srv, "root", "GET", "/fleet/exceptions", nil))
	if !strings.Contains(page, `id="webhooks"`) {
		t.Error("no webhooks section to link to")
	}
	for _, want := range []string{
		`data-hook="hook-fleet-row" data-id="7" data-repo="gamma" data-state="auto-paused"`,
		`data-hook="hook-fleet-row" data-id="9" data-repo="beta" data-state="failing"`,
		`data-hook="hook-fleet-row" data-id="11" data-repo="alpha" data-state="paused"`,
		`href="/r/gamma/settings#webhooks"`,
		`href="/r/beta/settings#webhooks"`,
	} {
		if !strings.Contains(page, want) {
			t.Errorf("missing %s", want)
		}
	}
	// Worst first: the order of the store is the order of the page.
	if strings.Index(page, `data-id="7"`) > strings.Index(page, `data-id="9"`) ||
		strings.Index(page, `data-id="9"`) > strings.Index(page, `data-id="11"`) {
		t.Error("rows are not in the order the store returned them")
	}
	if !strings.Contains(page, `data-hook="hook-fleet-summary" data-paused="2" data-failing="1"`) {
		t.Error("summary counts missing")
	}
}

// With every endpoint delivering there is nothing to say: no section,
// and no fleet tab of its own that would only ever say "all fine".
func TestFleetWebhooksQuietWhenHealthy(t *testing.T) {
	srv := hookFleetForge(t, fleetHooks{})
	page := body(as(t, srv, "root", "GET", "/fleet/exceptions", nil))
	if strings.Contains(page, `data-hook="hook-fleet"`) || strings.Contains(page, `data-hook="hook-fleet-row"`) {
		t.Error("a healthy fleet draws a webhooks section")
	}
	if strings.Contains(page, `href="/fleet/webhooks"`) {
		t.Error("the fleet nav still links a webhooks tab")
	}
	res := as(t, srv, "root", "GET", "/fleet/webhooks", nil)
	if loc := res.Header.Get("Location"); loc != "/fleet/exceptions#webhooks" {
		t.Errorf("old address went to %d %q", res.StatusCode, loc)
	}
}

// A subscription the forge paused says so on its row; one a person
// paused does not claim the forge did.
func TestSettingsRowShowsAutomaticPause(t *testing.T) {
	got := renderSettings(t, pageData{Repo: "alpha", HasHooks: true, Hooks: []HookSub{
		{ID: 7, URL: "https://a.test/h", Events: "*", PausedAt: "2026-11-04T10:00:00Z", PausedWhy: "5 in a row"},
		{ID: 8, URL: "https://b.test/h", Events: "*"},
	}})
	if strings.Count(got, `data-hook="hook-autopaused"`) != 1 {
		t.Errorf("auto-pause markers = %d, want 1", strings.Count(got, `data-hook="hook-autopaused"`))
	}
	if !strings.Contains(got, `data-hook="hook-autopaused" data-id="7"`) {
		t.Error("marker is not on the auto-paused row")
	}
}