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) } }