package web
import (
"io"
"net/http"
"net/url"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/ci"
)
// refusedAsNotFound holds a refusal to what a reader is owed: the
// not-found status and the not-found page inside the repository's own
// chrome. A recovered panic answers 500 with neither, which is how these
// tests once passed without ever seeing the refusal they are about.
func refusedAsNotFound(t *testing.T, resp *http.Response, what string) {
t.Helper()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusNotFound {
t.Errorf("FAIL: %s answered %d, want %d", what, resp.StatusCode, http.StatusNotFound)
}
for _, hook := range []string{`class="status"`, `class="repobar"`} {
if !strings.Contains(string(body), hook) {
t.Errorf("FAIL: %s: the not-found page lacks %s", what, hook)
}
}
}
func tokensFixture() []ci.WorkerToken {
return []ci.WorkerToken{
{ID: "own-token-1", Label: "alpha's runner", WorkerGroups: []string{ci.GroupFor("alpha")}},
{ID: "beta-token-1", Label: "beta's runner", WorkerGroups: []string{ci.GroupFor("beta")}},
{ID: "fleet-token-1", Label: "forge-host worker", TaskTypePrefixes: []string{"dantest-html"}},
}
}
// Which tokens a repository's settings may show: its own group's and
// nothing else. A fleet-wide token is the forge's, on the fleet
// settings page: listed on every repository's page, it showed a site
// admin tokens minted for other repositories, with their labels and the
// repositories they serve.
func TestWhichWorkerTokensARepositoryShows(t *testing.T) {
ts := tokensFixture()
for _, c := range []struct {
i int
want bool
}{
{0, true}, // alpha's own
{1, false}, // beta's: never alpha's
{2, false}, // fleet-wide: the fleet page's, not alpha's
} {
if got := workerTokenOf(ts[c.i], "alpha", time.Time{}); got != c.want {
t.Errorf("FAIL: %s on alpha: shown=%v, want %v", ts[c.i].ID, got, c.want)
}
}
if f := fleetWorkerTokens(ts); len(f) != 1 || f[0].ID != "fleet-token-1" {
t.Errorf("FAIL: the fleet page lists %v, want only the fleet-wide token", f)
}
}
// A REPOSITORY'S SETTINGS REVOKE ONLY ITS OWN WORKER TOKENS.
//
// The revoke beside each token worked from any repository the account
// administered, and every repository's page listed every token, so one
// repository's admin could revoke another repository's runners and the
// forge's own. Seven tokens that were not runner-probe's were revoked
// from runner-probe's page that way. Now a token that is not the
// repository's own answers not found and never reaches the engine.
func TestARepositoryRevokesOnlyItsOwnWorkerTokens(t *testing.T) {
eng := &mintRecorder{tokens: tokensFixture()}
srv := scopeServer(t, eng, "a8i")
for _, foreign := range []string{"beta-token-1", "fleet-token-1", "no-such-token"} {
resp := postAsDan(t, srv, "/r/alpha/settings/worker-tokens", url.Values{"revoke": {foreign}})
refusedAsNotFound(t, resp, "revoking "+foreign+" from alpha")
}
resp := postAsDan(t, srv, "/r/alpha/settings/worker-tokens", url.Values{"revoke": {"own-token-1"}})
if resp.StatusCode != http.StatusSeeOther {
t.Errorf("FAIL: revoking alpha's own token answered %d", resp.StatusCode)
}
eng.mu.Lock()
defer eng.mu.Unlock()
if len(eng.revoked) != 1 || eng.revoked[0] != "own-token-1" {
t.Errorf("FAIL: the engine was asked to revoke %v, want only alpha's own token", eng.revoked)
}
}
// A site admin revokes a fleet-wide token on the fleet page, and
// neither a repository's page nor the fleet page reaches another
// repository's token.
func TestFleetTokensAreRevokedOnTheFleetPageOnly(t *testing.T) {
eng := &mintRecorder{tokens: tokensFixture()}
srv := scopeServer(t, eng, "as")
refusedAsNotFound(t, postAsDan(t, srv, "/r/alpha/settings/worker-tokens", url.Values{"revoke": {"fleet-token-1"}}),
"a site admin revoking the fleet token from alpha's page")
if resp := postAsDan(t, srv, "/fleet/settings/worker-tokens", url.Values{"revoke": {"beta-token-1"}}); resp.StatusCode == http.StatusSeeOther {
t.Error("FAIL: the fleet page revoked a repository's own token")
}
if resp := postAsDan(t, srv, "/fleet/settings/worker-tokens", url.Values{"revoke": {"fleet-token-1"}}); resp.StatusCode != http.StatusSeeOther {
t.Errorf("FAIL: a site admin could not revoke a fleet token on the fleet page: %d", resp.StatusCode)
}
eng.mu.Lock()
defer eng.mu.Unlock()
if len(eng.revoked) != 1 || eng.revoked[0] != "fleet-token-1" {
t.Errorf("FAIL: the engine was asked to revoke %v, want only the fleet token", eng.revoked)
}
}
// A revoked token minted before the repository existed belongs to an
// earlier repository of the same name, whose deletion revoked it, and is
// not listed on its successor's page (quarry#294). A live one stays
// whatever its age: a repository removed and added back keeps its
// runners, and the page is where they are revoked. A fleet-wide token is
// not a repository's either way, and a stamp that will not parse is
// shown rather than guessed away.
func TestARevokedTokenOlderThanTheRepositoryIsNotItsOwn(t *testing.T) {
created := time.Date(2026, 9, 20, 12, 0, 0, 0, time.UTC)
group := []string{ci.GroupFor("alpha")}
const gone = "2026-09-19T09:00:00Z"
for _, c := range []struct {
tok ci.WorkerToken
want bool
}{
{ci.WorkerToken{ID: "old-revoked", WorkerGroups: group, CreatedAt: "2026-09-19T08:00:00Z", RevokedAt: gone}, false},
{ci.WorkerToken{ID: "old-live", WorkerGroups: group, CreatedAt: "2026-09-19T08:00:00Z"}, true},
{ci.WorkerToken{ID: "new-revoked", WorkerGroups: group, CreatedAt: "2026-09-21T08:00:00.123456Z", RevokedAt: gone}, true},
{ci.WorkerToken{ID: "odd", WorkerGroups: group, CreatedAt: "yesterday", RevokedAt: gone}, true},
{ci.WorkerToken{ID: "fleet", CreatedAt: "2026-09-19T08:00:00Z", RevokedAt: gone}, false},
} {
if got := workerTokenOf(c.tok, "alpha", created); got != c.want {
t.Errorf("FAIL: %s on alpha created %s: shown=%v, want %v", c.tok.ID, created, got, c.want)
}
}
// An unknown creation time filters nothing.
old := ci.WorkerToken{ID: "old", WorkerGroups: group, CreatedAt: "2026-09-19T08:00:00Z", RevokedAt: gone}
if !workerTokenOf(old, "alpha", time.Time{}) {
t.Error("FAIL: with no creation time known, alpha's token was hidden")
}
}