| 1 | package contratest |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "regexp" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | contra "github.com/carlosframework/contra/internal/contra" |
| 11 | ) |
| 12 | |
| 13 | // hashedStylesheet is the fingerprinted URL shape the index page |
| 14 | // links: /static/tokens.<16 hex>.css. |
| 15 | var hashedStylesheet = regexp.MustCompile(`/static/tokens\.[0-9a-f]{16}\.css`) |
| 16 | |
| 17 | func TestIndexRenders(t *testing.T) { |
| 18 | rec := get(t, newApp(t), "/") |
| 19 | if rec.Code != http.StatusOK { |
| 20 | t.Fatalf("GET /: status %d, want 200", rec.Code) |
| 21 | } |
| 22 | if !strings.Contains(rec.Body.String(), "the server knows nothing about you") { |
| 23 | t.Errorf("GET /: page is missing its heading:\n%s", rec.Body.String()) |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | func TestIndexLinksFingerprintedStylesheet(t *testing.T) { |
| 28 | h := newApp(t) |
| 29 | href := hashedStylesheet.FindString(get(t, h, "/").Body.String()) |
| 30 | if href == "" { |
| 31 | t.Fatal("index page links no fingerprinted stylesheet") |
| 32 | } |
| 33 | rec := get(t, h, href) |
| 34 | if rec.Code != http.StatusOK { |
| 35 | t.Fatalf("GET %s: status %d, want 200", href, rec.Code) |
| 36 | } |
| 37 | if got := rec.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" { |
| 38 | t.Errorf("GET %s: Cache-Control = %q, want immutable year", href, got) |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // The bare name keeps working, just never long-cached — so a changed |
| 43 | // file always shows on an ordinary reload. |
| 44 | func TestBareAssetNameStaysFresh(t *testing.T) { |
| 45 | rec := get(t, newApp(t), "/static/tokens.css") |
| 46 | if rec.Code != http.StatusOK { |
| 47 | t.Fatalf("GET /static/tokens.css: status %d, want 200", rec.Code) |
| 48 | } |
| 49 | if got := rec.Header().Get("Cache-Control"); got != "no-cache" { |
| 50 | t.Errorf("Cache-Control = %q, want no-cache", got) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // ErrorPage is what main.go points Options.ErrorPage at (and the seam |
| 55 | // to wire into Ctx.ErrorPage as handlers grow); called directly, it |
| 56 | // pins that a panic's page is the framework's error-page partial, not |
| 57 | // a stray net/http default. |
| 58 | func TestErrorPageRendersFrameworkPartial(t *testing.T) { |
| 59 | rec := httptest.NewRecorder() |
| 60 | contra.ErrorPage(rec, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusInternalServerError, "abc123") |
| 61 | if rec.Code != http.StatusInternalServerError { |
| 62 | t.Fatalf("ErrorPage: status %d, want %d", rec.Code, http.StatusInternalServerError) |
| 63 | } |
| 64 | body := rec.Body.String() |
| 65 | if !strings.Contains(body, "Something went wrong on our side") { |
| 66 | t.Errorf("ErrorPage body missing the 500 title:\n%s", body) |
| 67 | } |
| 68 | if !strings.Contains(body, "abc123") { |
| 69 | t.Errorf("ErrorPage body missing the ref:\n%s", body) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | // The home page shipped for a while saying "there is no signup, no vault and |
| 74 | // no stored data behind it" on a build that had all three. It was true when |
| 75 | // it was written and nobody re-read it as the app grew. |
| 76 | // |
| 77 | // So: the claims on the front page are pinned against the routes that exist. |
| 78 | // A page that says a thing does not exist, on a build where it does, is worse |
| 79 | // than no page — this is a product whose entire pitch is that its statements |
| 80 | // are literally true. |
| 81 | func TestHomePageDoesNotContradictTheApp(t *testing.T) { |
| 82 | h := newApp(t) |
| 83 | body := get(t, h, "/").Body.String() |
| 84 | |
| 85 | // If /signup answers, the home page must not claim there is no signup. |
| 86 | if get(t, h, "/signup").Code == http.StatusOK { |
| 87 | for _, lie := range []string{ |
| 88 | "no signup", "no vault and no stored data", "not open yet", |
| 89 | } { |
| 90 | if strings.Contains(strings.ToLower(body), lie) { |
| 91 | t.Errorf("the home page still says %q, but /signup works", lie) |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | // Every page must offer a way onward. An empty navigation rail is how the |
| 98 | // canary shipped, and a visitor who cannot reach signup from the front page |
| 99 | // has been handed a dead end. |
| 100 | func TestEveryPageOffersAWayOnward(t *testing.T) { |
| 101 | h := newApp(t) |
| 102 | for _, path := range []string{"/", "/signup", "/signin"} { |
| 103 | body := get(t, h, path).Body.String() |
| 104 | if !strings.Contains(body, `href="/signup"`) && !strings.Contains(body, `href="/vault"`) { |
| 105 | t.Errorf("%s offers no way onward: no link to signup or the vault", path) |
| 106 | } |
| 107 | if !strings.Contains(body, `href="/"`) { |
| 108 | t.Errorf("%s has no link home", path) |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | |