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
3import (
4 "bytes"
5 "encoding/json"
6 "html"
7 "log"
8 "net/http"
9 "net/url"
10 "os"
11 "regexp"
12 "strings"
13 "testing"
14
15 "github.com/carlosframework/platform/internal/geo"
16)
17
18func has(body, plain string) bool { return strings.Contains(body, html.EscapeString(plain)) }
19
20// newAppFormGolden is the base tree's own rendering of the create-app form
21// (the branch base, switch off), with the session's CSRF value blanked.
22const newAppFormGolden = `<form class="signin" method="post" action="/a/bab/apps/new">
23 <input type="hidden" name="csrf" value="CSRF">
24 <label class="visually-hidden" for="app-name">App name</label>
25 <input id="app-name" type="text" name="name" placeholder="my-app" autofocus
26 required pattern="[A-Za-z][A-Za-z0-9-]{0,31}" maxlength="32">
27 <button type="submit">Create app</button>
28</form>`
29
30// newAppForm is the rendered <form>…</form>, CSRF value blanked.
31func newAppForm(t *testing.T, body string) string {
32 t.Helper()
33 i := strings.Index(body, `<form class="signin"`)
34 if i < 0 {
35 t.Fatalf("no create-app form in %s", body)
36 }
37 j := strings.Index(body[i:], "</form>") + i + len("</form>")
38 return regexp.MustCompile(`name="csrf" value="[^"]*"`).ReplaceAllString(body[i:j], `name="csrf" value="CSRF"`)
39}
40
41// codex plan-R4 3, spec §10 and §16: whenever there is no picker, the form
42// is today's form byte for byte: with the switch off, and with the switch
43// on but only one eligible region.
44func TestNewAppPageWithoutPickerIsUnchanged(t *testing.T) {
45 off := newConsoleFixture(t)
46 one, _ := regionFixture(t)
47 setEdges(t, one, map[string]string{"uk-box": ukNoKey}) // the home alone is eligible
48 for name, fx := range map[string]*consoleFixture{"switch off": off, "one eligible region": one} {
49 _, _, body := get(t, signInPaul(t, fx), fx.u("/apps/new"))
50 if got := newAppForm(t, body); got != newAppFormGolden {
51 t.Errorf("%s: form =\n%q\nwant\n%q", name, got, newAppFormGolden)
52 }
53 }
54}
55
56func TestNewAppPickerAndAutoChoice(t *testing.T) {
57 fx, _ := regionFixture(t)
58 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-picker")
59 _, _, body := get(t, c, fx.u("/apps/new"))
60 for _, want := range []string{`name="region"`, `value="@auto"`, `value="uk"`, `value="ie"`} {
61 if !strings.Contains(body, want) {
62 t.Fatalf("picker missing %q", want)
63 }
64 }
65 if !has(body, ukPlace) || !has(body, regionCopy.PickerLabel) || !has(body, regionCopy.PickerHelp) {
66 t.Fatal("picker missing its copy")
67 }
68 if status, _, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"auto1"}, "region": {"@auto"}, "tz": {"Europe/London"}}); status != http.StatusSeeOther {
69 t.Fatalf("@auto post = %d", status)
70 }
71 if r, src, _ := fx.st.AppRegion(OpsAccountID, "auto1"); r != "uk" || src != "timezone" {
72 t.Fatalf("@auto = %q %q", r, src)
73 }
74 postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"chose"}, "region": {"ie"}, "tz": {"Europe/London"}})
75 if r, src, _ := fx.st.AppRegion(OpsAccountID, "chose"); r != "" || src != "chosen" {
76 t.Fatalf("explicit ie = %q %q", r, src)
77 }
78}
79
80// codex R2-3: a home NAMED "auto" is a region, not the sentinel.
81func TestRegionNamedAutoIsNotTheSentinel(t *testing.T) {
82 fx, _ := regionFixture(t)
83 fx.srv.cfg.HomeRegion = "auto"
84 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-auto-name")
85 postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"homeauto"}, "region": {"auto"}})
86 if r, src, _ := fx.st.AppRegion(OpsAccountID, "homeauto"); r != "" || src != "chosen" {
87 t.Fatalf("explicit home named auto = %q %q", r, src)
88 }
89}
90
91// Review Focus 3: uk's box loses the secrets key between render and POST.
92func TestNewAppPostWithANoLongerEligibleRegion(t *testing.T) {
93 fx, _ := regionFixture(t)
94 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-stale")
95 if _, _, body := get(t, c, fx.u("/apps/new")); !strings.Contains(body, `value="uk"`) {
96 t.Fatal("precondition: uk offered")
97 }
98 setEdges(t, fx, map[string]string{"uk-box": ukNoKey})
99 status, loc, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"stale"}, "region": {"uk"}})
100 if status != http.StatusSeeOther || loc != fx.path("/apps/new") {
101 t.Fatalf("stale region = %d %q, want back to the form", status, loc)
102 }
103 if _, _, body := get(t, c, fx.u("/apps/new")); !has(body, regionCopy.Unavailable) {
104 t.Fatal("no C5 flash")
105 }
106 if exists, _ := fx.st.HasAppIn(OpsAccountID, "stale"); exists {
107 t.Fatal("a refused region still created the app")
108 }
109}
110
111func TestNewAppRegionRouteNeedsSessionAndCSRF(t *testing.T) {
112 fx, _ := regionFixture(t)
113 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-route")
114 status, _, body := postForm(t, c, fx.u("/apps/new/region"), url.Values{"csrf": {csrf}, "tz": {"Europe/London"}})
115 var res map[string]string
116 if err := json.Unmarshal([]byte(body), &res); err != nil {
117 t.Fatalf("guess route body %q: %v", body, err)
118 }
119 if status != http.StatusOK || res["code"] != "uk" || res["source"] != "timezone" || res["name"] != ukPlace {
120 t.Fatalf("guess route = %d %v", status, res)
121 }
122 if status, _, _ := postForm(t, c, fx.u("/apps/new/region"), url.Values{"csrf": {"wrong"}, "tz": {"Europe/London"}}); status != http.StatusForbidden {
123 t.Fatalf("bad csrf = %d, want 403", status)
124 }
125 if status, _, _ := postForm(t, newBrowser(t), fx.u("/apps/new/region"), url.Values{"tz": {"Europe/London"}}); status == http.StatusOK {
126 t.Fatal("no session got a guess")
127 }
128}
129
130// §9.6 as 69a84224 made it: with the switch off the browser door neither
131// validates nor refuses an explicit region (resolveNewAppRegion stores the
132// home as "default"), exactly as the API door does.
133func TestNewAppExplicitRegionSkippedWithSwitchOff(t *testing.T) {
134 fx, _ := regionFixture(t)
135 fx.srv.cfg.AppRegions = false
136 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-off")
137 for _, region := range []string{"uk", "mars"} {
138 name := "off-" + region
139 status, loc, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {name}, "region": {region}})
140 if status != http.StatusSeeOther || loc == fx.path("/apps/new") {
141 t.Fatalf("switch off, region %q: %d %q, want the app created", region, status, loc)
142 }
143 if r, src, _ := fx.st.AppRegion(OpsAccountID, name); r != "" || src != "default" {
144 t.Fatalf("switch off, region %q stored %q %q, want \"\" default", region, r, src)
145 }
146 }
147}
148
149// With the switch on, a code that is not a region at all is refused with
150// C5 too, and nothing is created.
151func TestNewAppUnknownRegionIsRefused(t *testing.T) {
152 fx, _ := regionFixture(t)
153 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-unknown")
154 status, loc, _ := postForm(t, c, fx.u("/apps/new"), url.Values{"csrf": {csrf}, "name": {"nowhere"}, "region": {"mars!"}})
155 if status != http.StatusSeeOther || loc != fx.path("/apps/new") {
156 t.Fatalf("unknown region = %d %q", status, loc)
157 }
158 if _, _, body := get(t, c, fx.u("/apps/new")); !has(body, regionCopy.Unavailable) {
159 t.Fatal("no C5 flash")
160 }
161 if exists, _ := fx.st.HasAppIn(OpsAccountID, "nowhere"); exists {
162 t.Fatal("an unknown region still created the app")
163 }
164}
165
166// The picker's preselected option is "@auto", never a region code, so an
167// untouched form is not an explicit choice (adoption must never move).
168func TestNewAppPickerPreselectsAuto(t *testing.T) {
169 fx, _ := regionFixture(t)
170 status, _, body := get(t, signInPaul(t, fx), fx.u("/apps/new"))
171 if status != http.StatusOK {
172 t.Fatalf("form = %d", status)
173 }
174 if !strings.Contains(body, `<option value="@auto" selected>`) {
175 t.Fatalf("@auto is not the preselected option: %s", body)
176 }
177 if n := strings.Count(body, " selected>"); n != 1 {
178 t.Fatalf("%d selected options, want exactly 1", n)
179 }
180 if !strings.Contains(body, `id="app-region"`) || !strings.Contains(body, `name="tz"`) {
181 t.Fatal("picker has no region select or tz hint")
182 }
183 // The hints travel in POST bodies, never in a URL.
184 if !strings.Contains(body, `method: "POST"`) {
185 t.Fatal("the guess script does not POST")
186 }
187 if strings.Contains(body, "?tz=") || strings.Contains(body, "&tz=") {
188 t.Fatal("tz in a URL")
189 }
190}
191
192// C24 (Paul's ruling for DB-IP's licence): a link to db-ip.com carrying the
193// approved text, shown only when GeoIP is configured.
194func TestNewAppAttributionOnlyWithGeoIP(t *testing.T) {
195 fx, _ := regionFixture(t)
196 c := signInPaul(t, fx)
197 status, _, body := get(t, c, fx.u("/apps/new"))
198 if status != http.StatusOK || !strings.Contains(body, `id="app-region"`) {
199 t.Fatalf("precondition: picker page = %d", status)
200 }
201 if strings.Contains(body, "db-ip.com") || has(body, regionCopy.Attribution) {
202 t.Fatal("attribution shown with no GeoIP")
203 }
204 fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": geo.Point{Lat: 51.5, Lon: -0.12}}
205 _, _, body = get(t, c, fx.u("/apps/new"))
206 want := `<a href="https://db-ip.com">` + html.EscapeString("IP Geolocation by DB-IP") + `</a>`
207 if !strings.Contains(body, want) {
208 t.Fatalf("attribution link %q not in page", want)
209 }
210}
211
212// §6.5: the timezone and the address are never echoed, stored or logged.
213// PF9: assert the route answers 2xx before any negative check, and key the
214// page checks on an element id.
215func TestRegionGuessHintsNeverLeak(t *testing.T) {
216 fx, _ := regionFixture(t)
217 fx.srv.cfg.GeoIP = fakeLocator{"81.2.69.142": geo.Point{Lat: 51.5, Lon: -0.12}}
218 var logged bytes.Buffer
219 log.SetOutput(&logged)
220 t.Cleanup(func() { log.SetOutput(os.Stderr) })
221 c, csrf := signInAs(t, fx, "paul@keymail.dev", "code-leak")
222
223 const tz, ip = "Europe/London", "81.2.69.142"
224 req, err := http.NewRequest(http.MethodPost, fx.u("/apps/new/region"), strings.NewReader(url.Values{"csrf": {csrf}, "tz": {tz}}.Encode()))
225 if err != nil {
226 t.Fatal(err)
227 }
228 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
229 req.Header.Set("X-Forwarded-For", ip)
230 res, err := c.Do(req)
231 if err != nil {
232 t.Fatal(err)
233 }
234 var buf bytes.Buffer
235 buf.ReadFrom(res.Body)
236 res.Body.Close()
237 if res.StatusCode < 200 || res.StatusCode > 299 {
238 t.Fatalf("guess route = %d %s", res.StatusCode, buf.String())
239 }
240 if strings.Contains(buf.String(), tz) || strings.Contains(buf.String(), ip) {
241 t.Fatalf("guess answer echoes a hint: %s", buf.String())
242 }
243 if strings.Contains(logged.String(), tz) || strings.Contains(logged.String(), ip) {
244 t.Fatalf("a hint was logged: %s", logged.String())
245 }
246 // The form page carries no hint, and the tz field starts empty (the
247 // script fills it in the browser).
248 status, _, body := get(t, c, fx.u("/apps/new"))
249 if status != http.StatusOK {
250 t.Fatalf("form = %d", status)
251 }
252 if !strings.Contains(body, `id="app-region"`) || !strings.Contains(body, `<input type="hidden" name="tz" value="">`) {
253 t.Fatal("picker page without its empty tz field")
254 }
255 if strings.Contains(body, tz) || strings.Contains(body, ip) {
256 t.Fatal("form page echoes a hint")
257 }
258}
259