~bigbes/sr-ht-spec

ref: 128d72963281e275bb1ec3be2bdb8653a2bb0c78 sr-ht-spec/web/tokens_test.go -rw-r--r-- 5.7 KiB
128d7296 — Eugene Blikh ci: cache Go module and build dirs via cacher 13 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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)
	}
}