| 1 | package memoriatest |
| 2 | |
| 3 | import ( |
| 4 | "html" |
| 5 | "net/http" |
| 6 | "net/url" |
| 7 | "regexp" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "amadan.net/oficina/estilo.git/sky" |
| 12 | ) |
| 13 | |
| 14 | // The sign-in page is the suite's door (estilo chrome, 2026-09-29): one |
| 15 | // card on the team's sky with Memoria's mark and name at its head. Paul's |
| 16 | // ruling is that every app's sign-in is "a version of the Oficina sign-in |
| 17 | // screen", so this pins that the page IS the door rather than memoria's |
| 18 | // old bare form — and that everything the door links is actually served: |
| 19 | // a card whose stylesheet 404s, or a sky whose layers nothing mounts, |
| 20 | // renders as a plain form with no error anywhere. |
| 21 | // |
| 22 | // And the door's form must still sign someone in. Its field is named by |
| 23 | // estilo, not by this app, so the POST below uses the name the RENDERED |
| 24 | // page carries, not one typed here: if the door ever renamed the field, |
| 25 | // an owner's address would stop reaching the handler and this fails. |
| 26 | // |
| 27 | // MUTATION-TESTED 2026-09-29 by its author (so they prove consistency, not |
| 28 | // coverage — AGENTS.md), each reverted: the sky mount removed from app.go |
| 29 | // -> both served layers 404 here (and TestSkyRouteIsEstilosPrefix red); |
| 30 | // the chrome.css link removed from signin.html -> "does not link chrome.css". |
| 31 | func TestSigninPageIsTheDoor(t *testing.T) { |
| 32 | h, rec := signinApp(t) |
| 33 | rr := get(t, h, "/signin") |
| 34 | if rr.Code != http.StatusOK { |
| 35 | t.Fatalf("GET /signin = %d", rr.Code) |
| 36 | } |
| 37 | body := rr.Body.String() |
| 38 | |
| 39 | for _, want := range []string{ |
| 40 | "estilo-door", |
| 41 | `<span>Memoria</span>`, |
| 42 | `<h1>Sign in</h1>`, |
| 43 | // The suite's copy (shared chrome §9.2): the help under the field, |
| 44 | // and Open source reachable while signed out (§8.4). |
| 45 | "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", |
| 46 | `href="/help/open-source"`, |
| 47 | `action="/auth/begin"`, |
| 48 | // Paul, 2026-10-01: the door's button says "Sign in". |
| 49 | `>Sign in</button>`, |
| 50 | // The door's tab title (estilo chrome.Title). |
| 51 | `<title>Sign in · Memoria</title>`, |
| 52 | } { |
| 53 | if !strings.Contains(html.UnescapeString(body), html.UnescapeString(want)) { |
| 54 | t.Errorf("the sign-in page lacks %q", want) |
| 55 | } |
| 56 | } |
| 57 | // Retired (§9.2): "Email me a link" (Paul, 2026-10-01), the lede |
| 58 | // "Memoria holds your team's work…", and the |
| 59 | // footer's egress claim, which is about searching and is said only to |
| 60 | // somebody signed in. |
| 61 | // And the old title, "Sign in — Memoria". |
| 62 | for _, gone := range []string{"Email me a link", "holds your team", "<footer", "leaves this instance", "Sign in — Memoria"} { |
| 63 | if strings.Contains(body, gone) { |
| 64 | t.Errorf("the sign-in page still carries %q", gone) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // Every sky layer the door links: the two inline placeholders (which |
| 69 | // is why CSP must allow img-src data:) and the served ones. |
| 70 | layers := regexp.MustCompile(`<img estilo-sky-layer="([a-z]+)"[^>]* src="([^"]+)"`).FindAllStringSubmatch(body, -1) |
| 71 | if len(layers) < 3 { |
| 72 | t.Fatalf("found %d sky layers on the sign-in page, want the placeholders and the served layers", len(layers)) |
| 73 | } |
| 74 | served := 0 |
| 75 | for _, m := range layers { |
| 76 | src := html.UnescapeString(m[2]) |
| 77 | if strings.HasPrefix(src, "data:") { |
| 78 | continue |
| 79 | } |
| 80 | served++ |
| 81 | if !strings.HasPrefix(src, sky.Prefix) { |
| 82 | t.Errorf("sky layer %s is %q, not under %s", m[1], src, sky.Prefix) |
| 83 | } |
| 84 | if got := get(t, h, src); got.Code != http.StatusOK || got.Header().Get("Content-Type") != "image/svg+xml" { |
| 85 | t.Errorf("GET %s = %d %q; the sky layer is linked but not served", src, got.Code, got.Header().Get("Content-Type")) |
| 86 | } |
| 87 | } |
| 88 | if served == 0 { |
| 89 | t.Error("no sky layer is served from the app; the served half of the sky was never checked") |
| 90 | } |
| 91 | |
| 92 | // The door's stylesheets are linked and served. |
| 93 | for _, name := range []string{"chrome", "sky"} { |
| 94 | m := regexp.MustCompile(`href="(/static/` + name + `[^"]*\.css)"`).FindStringSubmatch(body) |
| 95 | if m == nil { |
| 96 | t.Errorf("the sign-in page does not link %s.css", name) |
| 97 | continue |
| 98 | } |
| 99 | if got := get(t, h, m[1]); got.Code != http.StatusOK || !strings.Contains(got.Body.String(), "estilo-") { |
| 100 | t.Errorf("GET %s = %d; the door's stylesheet is linked but not served", m[1], got.Code) |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // The field name the door renders, read off the page. |
| 105 | field := regexp.MustCompile(`<input rst-input type="email" id="estilo-signin-address" name="([^"]+)"`).FindStringSubmatch(body) |
| 106 | if field == nil { |
| 107 | t.Fatalf("no email field on the door:\n%s", body) |
| 108 | } |
| 109 | if field[1] != "address" { |
| 110 | t.Errorf("the door's field is named %q; beginGuard and rastrillo read address", field[1]) |
| 111 | } |
| 112 | rr = post(t, h, "/auth/begin", url.Values{field[1]: {dialTestOwner}}) |
| 113 | if rr.Code != http.StatusSeeOther || rr.Header().Get("Location") != "/signin?sent=1" { |
| 114 | t.Errorf("an owner's POST from the door = %d %q, want 303 /signin?sent=1", rr.Code, rr.Header().Get("Location")) |
| 115 | } |
| 116 | // A stranger's POST answers the same, so the redirect alone cannot |
| 117 | // tell a read address from an ignored one. The classifier dialling is |
| 118 | // what proves the owner's address reached sign-in. |
| 119 | if !rec.dialed() { |
| 120 | t.Error("the owner's address never reached the sign-in classifier; the door's field is not what the handler reads") |
| 121 | } |
| 122 | } |
| 123 | |