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
}
// The fleet view is for site admins, lists every paused or failing
// endpoint with the repository it belongs to, and links 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/webhooks", nil); res.StatusCode == 200 {
t.Fatal("anonymous reached the fleet webhooks view")
}
page := body(as(t, srv, "root", "GET", "/fleet/webhooks", nil))
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")
}
}
func TestFleetWebhooksEmpty(t *testing.T) {
srv := hookFleetForge(t, fleetHooks{})
page := body(as(t, srv, "root", "GET", "/fleet/webhooks", nil))
if !strings.Contains(page, `data-hook="hook-fleet-empty"`) || strings.Contains(page, `data-hook="hook-fleet-row"`) {
t.Error("an empty fleet is not drawn as empty")
}
}
// 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")
}
}