~bigbes/sr-ht-spec

ref: b643b0be64833bb1c989b70b57a5a7ded7142904 sr-ht-spec/web/proposal_test.go -rw-r--r-- 7.6 KiB
b643b0be — Eugene Blikh bearer: refuse through the shared table and challenge 9 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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package web

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
	"sourcecraft.dev/bigbes/sr-ht-spec/service"
)

// seedProposal registers a proposal and its diff on the fake reader.
func seedProposal(r *fakeReader, p service.Proposal, docs []service.ProposalDoc) {
	r.proposals[p.ID] = p
	r.diffs[p.ID] = docs
}

func openProposal() service.Proposal {
	return service.Proposal{
		ID: 7, Space: demoSpace, Title: "Revise storage model",
		Rationale: "clearer wording", BaseRev: headRev, Branch: "proposals/7",
		State: core.StateOpen, Agent: "claude-code/spec-writer", AgentSession: "sess-1",
	}
}

// post issues a form POST as a user, with the Origin header set unless overridden.
func post(t *testing.T, h http.Handler, target, user, origin string) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, target, nil)
	if user != "" {
		login(req, user)
	}
	if origin != "" {
		req.Header.Set("Origin", origin)
	}
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	return rec
}

// TestProposalPageRendersDiffAndControls proves the owner sees the diff and the
// approve/reject controls on an open proposal.
func TestProposalPageRendersDiffAndControls(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), []service.ProposalDoc{{
		Path:     "specs/0007-storage.md",
		Base:     []byte("# Storage\n\nGit is authoritative here.\n"),
		Proposed: []byte("# Storage\n\nGit is the authoritative source here.\n"),
	}})
	h, _, _ := testServerWith(t, r)

	rec := get(t, h, "/~bigbes/rfcs/p/7", "bigbes")
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200; body:\n%s", rec.Code, rec.Body)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "prosediff") {
		t.Errorf("page has no rendered diff; body:\n%s", body)
	}
	if !strings.Contains(body, "/p/7/approve") || !strings.Contains(body, "/p/7/reject") {
		t.Errorf("owner viewing an open proposal has no approve/reject controls")
	}
	if !strings.Contains(body, "specs/0007-storage.md") {
		t.Errorf("page does not name the changed document")
	}
	// The review page is the one full-bleed page of this surface: two prose
	// columns side by side do not fit the centred container.
	if !strings.Contains(body, `class="container-fluid"`) {
		t.Errorf("the review page is not full width; body:\n%s", body)
	}
}

// TestProposalPageHidesControlsWhenMerged proves a terminal proposal shows no
// controls and states its outcome.
func TestProposalPageHidesControlsWhenMerged(t *testing.T) {
	r := newFakeReader()
	p := openProposal()
	p.State = core.StateMerged
	p.Approval = core.ApprovalHuman
	p.MergedRev = oldRev
	seedProposal(r, p, nil)
	h, _, _ := testServerWith(t, r)

	rec := get(t, h, "/~bigbes/rfcs/p/7", "bigbes")
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", rec.Code)
	}
	if strings.Contains(rec.Body.String(), "/p/7/approve") {
		t.Errorf("a merged proposal still shows the approve control")
	}
}

// TestProposalPageWrongSpaceIs404 proves a proposal id addressed through the
// wrong space's URL is not found.
func TestProposalPageWrongSpaceIs404(t *testing.T) {
	r := newFakeReader()
	p := openProposal()
	p.Space = core.SpaceRef{Owner: "bigbes", Name: "other"}
	seedProposal(r, p, nil)
	h, _, _ := testServerWith(t, r)

	rec := get(t, h, "/~bigbes/rfcs/p/7", "bigbes")
	if rec.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want 404 for a proposal in another space", rec.Code)
	}
}

// TestProposalPageNonNumericIs404 proves the "/p/" namespace refuses a
// non-numeric id rather than treating it as a document.
func TestProposalPageNonNumericIs404(t *testing.T) {
	h, _, _ := testServerWith(t, newFakeReader())
	rec := get(t, h, "/~bigbes/rfcs/p/not-a-number", "bigbes")
	if rec.Code != http.StatusNotFound {
		t.Fatalf("status = %d, want 404", rec.Code)
	}
}

// TestProposalPageAnonymousRedirected proves a viewer with no read authority is
// sent to login, not shown the proposal.
func TestProposalPageAnonymousRedirected(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), nil)
	h, _, _ := testServerWith(t, r)
	rec := get(t, h, "/~bigbes/rfcs/p/7", "")
	if rec.Code != http.StatusSeeOther && rec.Code != http.StatusFound {
		t.Fatalf("status = %d, want a login redirect", rec.Code)
	}
}

// TestApproveMergesAsOwner proves the owner's approve POST merges the proposal
// and redirects back.
func TestApproveMergesAsOwner(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), nil)
	h, reader, _ := testServerWith(t, r)

	rec := post(t, h, "/~bigbes/rfcs/p/7/approve", "bigbes", "https://spec.example")
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("status = %d, want 303; body:\n%s", rec.Code, rec.Body)
	}
	if got := reader.proposals[7].State; got != core.StateMerged {
		t.Errorf("proposal state = %s, want merged", got)
	}
	if loc := rec.Header().Get("Location"); loc != "/~bigbes/rfcs/p/7" {
		t.Errorf("redirect = %q, want the proposal page", loc)
	}
}

// TestRejectResolvesAsOwner proves the owner's reject POST rejects the proposal.
func TestRejectResolvesAsOwner(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), nil)
	h, reader, _ := testServerWith(t, r)

	rec := post(t, h, "/~bigbes/rfcs/p/7/reject", "bigbes", "https://spec.example")
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("status = %d, want 303", rec.Code)
	}
	if got := reader.proposals[7].State; got != core.StateRejected {
		t.Errorf("proposal state = %s, want rejected", got)
	}
}

// TestApproveForbiddenForAgent proves an agent — authenticated but not the owner
// — may not approve.
func TestApproveForbiddenForAgent(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), nil)
	h, reader, _ := testServerWith(t, r)

	req := httptest.NewRequest(http.MethodPost, "/~bigbes/rfcs/p/7/approve", nil)
	req.Header.Set("Authorization", "Bearer "+agentTk)
	req.Header.Set("Origin", "https://spec.example")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403 for an agent approving", rec.Code)
	}
	if reader.proposals[7].State != core.StateOpen {
		t.Errorf("the proposal was resolved despite the agent being refused")
	}
}

// TestApproveRefusedCrossOrigin proves a POST whose Origin is not this site is
// refused — the CSRF defense.
func TestApproveRefusedCrossOrigin(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), nil)
	h, reader, _ := testServerWith(t, r)

	rec := post(t, h, "/~bigbes/rfcs/p/7/approve", "bigbes", "https://evil.example")
	if rec.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403 for a cross-origin POST", rec.Code)
	}
	if reader.proposals[7].State != core.StateOpen {
		t.Errorf("the proposal was resolved despite the cross-origin refusal")
	}
}

// TestApproveMissingOriginRefused proves a POST with no Origin or Referer is
// refused rather than trusted.
func TestApproveMissingOriginRefused(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), nil)
	h, _, _ := testServerWith(t, r)
	rec := post(t, h, "/~bigbes/rfcs/p/7/approve", "bigbes", "")
	if rec.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403 when no Origin is presented", rec.Code)
	}
}

// TestApproveStaleIs409 proves a merge that the service reports stale surfaces as
// a 409, not a 500.
func TestApproveStaleIs409(t *testing.T) {
	r := newFakeReader()
	seedProposal(r, openProposal(), nil)
	r.actErr = service.ErrStale
	h, _, _ := testServerWith(t, r)

	rec := post(t, h, "/~bigbes/rfcs/p/7/approve", "bigbes", "https://spec.example")
	if rec.Code != http.StatusConflict {
		t.Fatalf("status = %d, want 409 for a stale approve", rec.Code)
	}
}