carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// The browser Settings door for domain attach/detach (appdomains.go, #82
4// Task 4): thin handlers over the attachDomain/detachDomain core proven in
5// domainattach_test.go. What these tests own is the door's OWN shape —
6// session, CSRF, the owner-only gate matching handleAppDelete's, and
7// detach's typed-host confirm — not the attach/detach policy itself.
8
9import (
10 "context"
11 "net/http"
12 "net/url"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/domains"
18 "github.com/carlosframework/platform/internal/edgesfile"
19 "github.com/carlosframework/platform/internal/status"
20)
21
22// TestAppDomainAttachHappyPathAliases exercises the default fixture's kass
23// app, which already carries one ready route (kass.carlosapp.dev, view_test's
24// buildFixtures) — the alias branch needs nothing more set up than that.
25func TestAppDomainAttachHappyPathAliases(t *testing.T) {
26 fx := newConsoleFixture(t)
27 c := signInPaul(t, fx)
28 csrf := sessionToken(t, fx, c)
29 const host = "docs.example.com"
30
31 status, loc, body := postForm(t, c, fx.u("/apps/kass/domains/attach"),
32 url.Values{"csrf": {csrf}, "host": {host}})
33 if status != http.StatusSeeOther || loc != fx.path("/apps/kass/settings/domains") {
34 t.Fatalf("attach = %d -> %q, want 303 -> settings:\n%s", status, loc, body)
35 }
36 claim, err := domains.GetClaim(context.Background(), fx.rel, host)
37 if err != nil {
38 t.Fatalf("claim: %v", err)
39 }
40 if claim.Account != fx.sqid || claim.App != "kass" || claim.Route == "" {
41 t.Errorf("claim = %+v, want an alias claim naming kass's existing route", claim)
42 }
43}
44
45// TestAppDomainAttachNeedsCSRF matches TestRenameHandlerNeedsCSRF's shape: a
46// cross-site request is refused before the host is ever looked at, and
47// nothing is claimed.
48func TestAppDomainAttachNeedsCSRF(t *testing.T) {
49 fx := newConsoleFixture(t)
50 c := signInPaul(t, fx)
51 const host = "docs.example.com"
52
53 status, _, body := postForm(t, c, fx.u("/apps/kass/domains/attach"),
54 url.Values{"csrf": {"not-the-token"}, "host": {host}})
55 if status != http.StatusForbidden {
56 t.Fatalf("attach with a bad csrf = %d, want 403:\n%s", status, body)
57 }
58 if !strings.Contains(body, "Cross-site request blocked.") {
59 t.Fatalf("403 body does not name the reason:\n%s", body)
60 }
61 claimAbsent(t, fx, host)
62}
63
64// TestAppDomainAttachLowercasesHost: a member typing Docs.Example.com must
65// not be refused by ValidHostShape's lowercase-only label regex — the door
66// normalizes case before the host ever reaches the core, and the claim
67// lands at the normalized key, not the one typed.
68func TestAppDomainAttachLowercasesHost(t *testing.T) {
69 fx := newConsoleFixture(t)
70 c := signInPaul(t, fx)
71 csrf := sessionToken(t, fx, c)
72 const mixed = "Docs.Example.com"
73 const lower = "docs.example.com"
74
75 status, loc, body := postForm(t, c, fx.u("/apps/kass/domains/attach"),
76 url.Values{"csrf": {csrf}, "host": {mixed}})
77 if status != http.StatusSeeOther || loc != fx.path("/apps/kass/settings/domains") {
78 t.Fatalf("attach with a mixed-case host = %d -> %q, want 303 -> settings:\n%s", status, loc, body)
79 }
80 if _, err := domains.GetClaim(context.Background(), fx.rel, lower); err != nil {
81 t.Errorf("claim at the lowercase key: %v, want it there", err)
82 }
83 if _, err := domains.GetClaim(context.Background(), fx.rel, mixed); err == nil {
84 t.Error("a claim also landed at the mixed-case key — the door did not normalize")
85 }
86}
87
88// TestAppDomainAttachIsOwnerOnly matches TestAppDeleteIsOwnerOnly's shape:
89// a member gets the same byte-identical 404 a total stranger would, and
90// nothing is claimed.
91func TestAppDomainAttachIsOwnerOnly(t *testing.T) {
92 fx := newConsoleFixture(t)
93 if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
94 t.Fatal(err)
95 }
96 fx.setUser("sam@keymail.dev")
97 c := newBrowser(t)
98 signUp(t, fx, c, "code-sam-appdomainattach")
99 csrf := sessionToken(t, fx, c)
100 const host = "docs.example.com"
101
102 status, _, memberBody := postForm(t, c, fx.u("/apps/kass/domains/attach"),
103 url.Values{"csrf": {csrf}, "host": {host}})
104 if status != http.StatusNotFound {
105 t.Fatalf("member attaching a domain = %d, want 404", status)
106 }
107 strangerBody := strangerRefusal(t, fx, "code-zoe-appdomainattach", fx.u("/apps/kass/domains/attach"),
108 url.Values{"host": {host}})
109 if memberBody != strangerBody {
110 t.Fatalf("the two refusals are not byte-identical:\n(member) %q\n(stranger) %q", memberBody, strangerBody)
111 }
112 claimAbsent(t, fx, host)
113}
114
115// domainAttachedFixture attaches host to app via the Task 3 core directly
116// (the doors' shared, already-tested path) and returns the fixture and
117// signed-in owner client — the setup every detach test needs and none of
118// them is testing.
119func domainAttachedFixture(t *testing.T, app, host string) (*consoleFixture, *http.Client, string) {
120 t.Helper()
121 fx := newConsoleFixture(t)
122 acct, err := fx.st.AccountBySqid(fx.sqid)
123 if err != nil || acct == nil {
124 t.Fatalf("fixture account %s: %v", fx.sqid, err)
125 }
126 if _, ref := fx.srv.attachDomain(context.Background(), acct, app, host, "", "", "paul@keymail.dev"); ref != nil {
127 t.Fatalf("attach setup: refused %+v", ref)
128 }
129 c := signInPaul(t, fx)
130 csrf := sessionToken(t, fx, c)
131 return fx, c, csrf
132}
133
134// TestAppDomainDetachNeedsMatchingConfirm is the binding detail from the
135// brief: a confirm that does not equal the host is refused 422 BEFORE the
136// core is ever called, so the claim must survive untouched.
137func TestAppDomainDetachNeedsMatchingConfirm(t *testing.T) {
138 const host = "docs.example.com"
139 fx, c, csrf := domainAttachedFixture(t, "kass", host)
140
141 status, _, body := postForm(t, c, fx.u("/apps/kass/domains/detach"),
142 url.Values{"csrf": {csrf}, "host": {host}, "confirm": {"not-" + host}})
143 if status != http.StatusUnprocessableEntity {
144 t.Fatalf("detach with a wrong confirm = %d, want 422:\n%s", status, body)
145 }
146 if _, err := domains.GetClaim(context.Background(), fx.rel, host); err != nil {
147 t.Errorf("claim after a refused detach: %v, want it untouched", err)
148 }
149}
150
151// TestAppDomainDetachHappyPath: the typed confirm matches, the claim goes,
152// and the member lands back on Settings.
153func TestAppDomainDetachHappyPath(t *testing.T) {
154 const host = "docs.example.com"
155 fx, c, csrf := domainAttachedFixture(t, "kass", host)
156
157 status, loc, body := postForm(t, c, fx.u("/apps/kass/domains/detach"),
158 url.Values{"csrf": {csrf}, "host": {host}, "confirm": {host}})
159 if status != http.StatusSeeOther || loc != fx.path("/apps/kass/settings/domains") {
160 t.Fatalf("detach = %d -> %q, want 303 -> settings:\n%s", status, loc, body)
161 }
162 claimAbsent(t, fx, host)
163}
164
165// TestAppDomainDetachUnattachedHostIs404 exercises renderDoorRefusal's 404
166// branch: the owner's own gates all pass (CSRF, RoleOwner, confirm == host),
167// but the core has nothing to release, so its doorRefusal{404,...} must
168// render as the stranger's own byte-identical 404 — never a message that
169// would tell a prober "an app exists here, but this host does not".
170func TestAppDomainDetachUnattachedHostIs404(t *testing.T) {
171 fx := newConsoleFixture(t)
172 c := signInPaul(t, fx)
173 csrf := sessionToken(t, fx, c)
174 const host = "never-attached.example.com"
175
176 status, _, body := postForm(t, c, fx.u("/apps/kass/domains/detach"),
177 url.Values{"csrf": {csrf}, "host": {host}, "confirm": {host}})
178 if status != http.StatusNotFound {
179 t.Fatalf("detach of an unattached host = %d, want 404:\n%s", status, body)
180 }
181 strangerBody := strangerRefusal(t, fx, "code-zoe-appdomaindetach-unattached", fx.u("/apps/kass/domains/detach"),
182 url.Values{"host": {host}, "confirm": {host}})
183 if body != strangerBody {
184 t.Fatalf("owner's unattached-host 404 is not byte-identical to the stranger's:\n(owner) %q\n(stranger) %q", body, strangerBody)
185 }
186 claimAbsent(t, fx, host)
187}
188
189// TestAppDomainDetachIsOwnerOnly mirrors the attach case: a member gets the
190// same byte-identical 404 a stranger would.
191func TestAppDomainDetachIsOwnerOnly(t *testing.T) {
192 const host = "docs.example.com"
193 fx, _, _ := domainAttachedFixture(t, "kass", host)
194 if err := fx.st.AddMember(OpsAccountID, "sam@keymail.dev", RoleMember, time.Now()); err != nil {
195 t.Fatal(err)
196 }
197 fx.setUser("sam@keymail.dev")
198 sc := newBrowser(t)
199 signUp(t, fx, sc, "code-sam-appdomaindetach")
200 scsrf := sessionToken(t, fx, sc)
201
202 status, _, memberBody := postForm(t, sc, fx.u("/apps/kass/domains/detach"),
203 url.Values{"csrf": {scsrf}, "host": {host}, "confirm": {host}})
204 if status != http.StatusNotFound {
205 t.Fatalf("member detaching a domain = %d, want 404", status)
206 }
207 strangerBody := strangerRefusal(t, fx, "code-zoe-appdomaindetach", fx.u("/apps/kass/domains/detach"),
208 url.Values{"host": {host}, "confirm": {host}})
209 if memberBody != strangerBody {
210 t.Fatalf("the two refusals are not byte-identical:\n(member) %q\n(stranger) %q", memberBody, strangerBody)
211 }
212 if _, err := domains.GetClaim(context.Background(), fx.rel, host); err != nil {
213 t.Errorf("claim after a member's refused detach: %v, want it untouched", err)
214 }
215}
216
217// TestAppDomainAttachRefusesADeploymentRouteNeutrally: the BROWSER door's
218// half of the fleet-wide route collision (Task 5 review round 2). Both doors
219// share attachDomain, which is where the guard now lives — immediately
220// before each of its two AttachClaim calls — so this door is covered by the
221// same move that closed the CLI's kind=static branch. This test is what
222// makes that coverage a fact rather than an inference.
223//
224// The property is the one the CLI door keeps: a hostname a live box already
225// serves as a REAL route is refused with the refusal a hostname held by
226// another account gets, and the two are indistinguishable. Compared page
227// against page, not against a literal, so they cannot drift apart.
228func TestAppDomainAttachRefusesADeploymentRouteNeutrally(t *testing.T) {
229 ctx := context.Background()
230 fx := newConsoleFixture(t)
231 c := signInPaul(t, fx)
232 csrf := sessionToken(t, fx, c)
233
234 // go.tito.io is a real, NON-ALIAS route published by a live box. Both
235 // halves are required: DeploymentRoutes drops any edges file whose box
236 // has no fresh status snapshot.
237 if _, err := fx.rel.Put(ctx, edgesfile.Key("annie"),
238 []byte(`{"v":1,"box":"annie","eip":"198.51.100.9","hosts":{"go.tito.io":{"role":"home"}}}`), ""); err != nil {
239 t.Fatal(err)
240 }
241 if err := status.WriteBox(ctx, fx.rel, status.Box{Label: "annie", GeneratedAt: time.Now()}); err != nil {
242 t.Fatal(err)
243 }
244 // The yardstick: a hostname held by a different account entirely.
245 if _, _, err := domains.AttachClaim(ctx, fx.rel, domains.Claim{
246 Host: "claimed-by-someone-else.com", Account: "cdef", App: "widgets",
247 Route: "widgets.example.net", CreatedAt: time.Now().UTC().Format(time.RFC3339),
248 }); err != nil {
249 t.Fatal(err)
250 }
251
252 routeStatus, _, routeBody := postForm(t, c, fx.u("/apps/kass/domains/attach"),
253 url.Values{"csrf": {csrf}, "host": {"go.tito.io"}})
254 heldStatus, _, heldBody := postForm(t, c, fx.u("/apps/kass/domains/attach"),
255 url.Values{"csrf": {csrf}, "host": {"claimed-by-someone-else.com"}})
256
257 if routeStatus != http.StatusConflict {
258 t.Fatalf("browser attach of another box's route = %d, want 409:\n%s", routeStatus, routeBody)
259 }
260 if routeStatus != heldStatus || routeBody != heldBody {
261 t.Fatalf("browser refusal %d differs from the held-hostname refusal %d — that is an existence oracle:\n--- route ---\n%s\n--- held ---\n%s",
262 routeStatus, heldStatus, routeBody, heldBody)
263 }
264 claimAbsent(t, fx, "go.tito.io")
265}
266
267// The browser door's own version of the ownership exemption: a hostname this
268// app ALREADY holds the claim on stays attachable, because a first-route
269// attach makes the host a real route and the fold would otherwise report the
270// caller's own route back at them forever.
271func TestAppDomainAttachStillConvergesTheCallersOwnDeploymentRoute(t *testing.T) {
272 ctx := context.Background()
273 fx := newConsoleFixture(t)
274 c := signInPaul(t, fx)
275 csrf := sessionToken(t, fx, c)
276
277 if _, err := fx.rel.Put(ctx, edgesfile.Key("annie"),
278 []byte(`{"v":1,"box":"annie","eip":"198.51.100.9","hosts":{"mine.example.com":{"role":"home"}}}`), ""); err != nil {
279 t.Fatal(err)
280 }
281 if err := status.WriteBox(ctx, fx.rel, status.Box{Label: "annie", GeneratedAt: time.Now()}); err != nil {
282 t.Fatal(err)
283 }
284 if _, _, err := domains.AttachClaim(ctx, fx.rel, domains.Claim{
285 Host: "mine.example.com", Account: fx.sqid, App: "kass",
286 CreatedAt: time.Now().UTC().Format(time.RFC3339),
287 }); err != nil {
288 t.Fatal(err)
289 }
290
291 st, loc, body := postForm(t, c, fx.u("/apps/kass/domains/attach"),
292 url.Values{"csrf": {csrf}, "host": {"mine.example.com"}})
293 if st != http.StatusSeeOther || loc != fx.path("/apps/kass/settings/domains") {
294 t.Fatalf("re-attach of the caller's own hostname = %d -> %q, want 303 -> settings:\n%s", st, loc, body)
295 }
296}
297