package web
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"sourcecraft.dev/bigbes/sr-ht-spec/service"
)
// selfOrigin is this instance's origin as testServerWith configures it — the
// value the cross-site guard accepts.
const selfOrigin = "https://spec.example"
func TestTokensPageListsWhatExists(t *testing.T) {
r := newFakeReader()
revoked := time.Date(2026, 8, 1, 10, 0, 0, 0, time.UTC)
r.tokens = []service.AgentToken{
{ID: 2, Name: "claude-code", Created: time.Date(2026, 8, 2, 9, 0, 0, 0, time.UTC)},
{ID: 1, Name: "old-runner", Created: time.Date(2026, 7, 1, 9, 0, 0, 0, time.UTC), Revoked: &revoked},
}
h, _, _ := testServerWith(t, r)
rec := get(t, h, "/tokens", "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body:\n%s", rec.Code, rec.Body)
}
body := rec.Body.String()
for _, want := range []string{"claude-code", "old-runner", "active", "revoked", "/tokens/2/revoke"} {
if !strings.Contains(body, want) {
t.Errorf("the page does not mention %q; body:\n%s", want, body)
}
}
// A revoked token has nothing left to revoke.
if strings.Contains(body, "/tokens/1/revoke") {
t.Errorf("the page offers to revoke an already-revoked token")
}
}
func TestTokensPageEmpty(t *testing.T) {
h, _, _ := testServerWith(t, newFakeReader())
rec := get(t, h, "/tokens", "bigbes")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
if !strings.Contains(rec.Body.String(), "No tokens yet") {
t.Errorf("an empty inventory does not say so:\n%s", rec.Body)
}
}
// TestTokensPageIsOwnerOnly is the whole point of the page's ACL: an agent is
// authenticated, and still may not see the inventory of who can write — nor be
// bounced to a login page it has no way to use.
func TestTokensPageIsOwnerOnly(t *testing.T) {
h, _, _ := testServerWith(t, newFakeReader())
if rec := getAgent(t, h, "/tokens", agentTk); rec.Code != http.StatusForbidden {
t.Errorf("an agent got %d for /tokens, want 403", rec.Code)
}
rec := get(t, h, "/tokens", "")
if rec.Code != http.StatusSeeOther && rec.Code != http.StatusFound {
t.Errorf("an anonymous browser got %d, want a login redirect", rec.Code)
}
}
// TestTokenMintShowsThePlaintextOnce proves the response to the mint carries the
// value the service returned. It is shown here or nowhere: nothing stores it.
func TestTokenMintShowsThePlaintextOnce(t *testing.T) {
r := newFakeReader()
h, _, _ := testServerWith(t, r)
rec := postForm(t, h, "/tokens", "bigbes", selfOrigin, url.Values{"name": {"claude-code"}})
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body:\n%s", rec.Code, rec.Body)
}
if len(r.issued) != 1 {
t.Fatalf("the service minted %d tokens, want 1", len(r.issued))
}
body := rec.Body.String()
if !strings.Contains(body, r.issued[0]) {
t.Errorf("the minted token is not on the page; body:\n%s", body)
}
if !strings.Contains(body, "only time it is shown") {
t.Errorf("the page does not warn that the token is shown once")
}
// A later view of the page must not carry it: it exists only in the
// response to the request that minted it.
if again := get(t, h, "/tokens", "bigbes"); strings.Contains(again.Body.String(), r.issued[0]) {
t.Errorf("a later page view still shows the plaintext:\n%s", again.Body)
}
}
func TestTokenMintRefusesANamelessToken(t *testing.T) {
r := newFakeReader()
h, _, _ := testServerWith(t, r)
rec := postForm(t, h, "/tokens", "bigbes", selfOrigin, url.Values{"name": {" "}})
if rec.Code == http.StatusOK {
t.Errorf("a blank name was accepted: %d", rec.Code)
}
if len(r.issued) != 0 {
t.Errorf("a token was minted for a blank name: %v", r.issued)
}
}
func TestTokenWritesAreOwnerOnlyAndSameOrigin(t *testing.T) {
r := newFakeReader()
r.tokens = []service.AgentToken{{ID: 1, Name: "claude-code", Created: time.Now()}}
h, _, _ := testServerWith(t, r)
// An agent may not mint, even with a valid bearer token.
req := httptest.NewRequest(http.MethodPost, "/tokens", strings.NewReader("name=self"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", "Bearer "+agentTk)
req.Header.Set("Origin", selfOrigin)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("an agent minting got %d, want 403", rec.Code)
}
// The owner's own form post from somewhere else is a forgery.
if rec := postForm(t, h, "/tokens", "bigbes", "https://evil.example",
url.Values{"name": {"x"}}); rec.Code != http.StatusForbidden {
t.Errorf("a cross-origin mint got %d, want 403", rec.Code)
}
if rec := post(t, h, "/tokens/1/revoke", "bigbes", "https://evil.example"); rec.Code != http.StatusForbidden {
t.Errorf("a cross-origin revoke got %d, want 403", rec.Code)
}
if len(r.issued) != 0 {
t.Errorf("a refused request still minted: %v", r.issued)
}
if !r.tokens[0].Active() {
t.Errorf("a refused request still revoked the token")
}
}
func TestTokenRevokeStampsAndRedirects(t *testing.T) {
r := newFakeReader()
r.tokens = []service.AgentToken{{ID: 4, Name: "claude-code", Created: time.Now()}}
h, _, _ := testServerWith(t, r)
rec := post(t, h, "/tokens/4/revoke", "bigbes", selfOrigin)
if rec.Code != http.StatusSeeOther {
t.Fatalf("status = %d, want 303; body:\n%s", rec.Code, rec.Body)
}
if got := rec.Header().Get("Location"); got != "/tokens" {
t.Errorf("Location = %q want /tokens", got)
}
if r.tokens[0].Active() {
t.Errorf("the token is still active after a revoke")
}
}
func TestTokenRevokeRejectsAMalformedID(t *testing.T) {
h, _, _ := testServerWith(t, newFakeReader())
if rec := post(t, h, "/tokens/abc/revoke", "bigbes", selfOrigin); rec.Code != http.StatusNotFound {
t.Errorf("status = %d, want 404", rec.Code)
}
}