package web import ( "net/http" "net/http/httptest" "strings" "testing" ) func get(t *testing.T, path string) *httptest.ResponseRecorder { t.Helper() h, err := Handler() if err != nil { t.Fatal(err) } rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", path, nil)) return rec } func TestTheRootServesTheApp(t *testing.T) { rec := get(t, "/") if rec.Code != http.StatusOK { t.Fatalf("got %d", rec.Code) } if !strings.Contains(rec.Body.String(), "<") { t.Fatal("no markup came back") } } // A deep link or a browser reload must land on the app. Returning 404 for a // path the client router owns is the classic single-page-app deployment bug, // and it only shows up when somebody refreshes a page that is not the root - // which is to say, in front of a customer. func TestAnyClientRouteReturnsTheApp(t *testing.T) { for _, path := range []string{"/customers", "/reports", "/sites/abc/live"} { rec := get(t, path) if rec.Code != http.StatusOK { t.Errorf("%s: got %d, want the app", path, rec.Code) } if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { t.Errorf("%s: content-type %q", path, ct) } } } // Caching the entry document is how a browser ends up running last week's // bundle against this week's API - a version skew nobody can reproduce because // it depends on one machine's cache. func TestTheEntryDocumentIsNeverCached(t *testing.T) { if got := get(t, "/").Header().Get("Cache-Control"); got != "no-store" { t.Fatalf("Cache-Control %q, want no-store", got) } } // A path that climbs out of the bundle must not reach the filesystem. Cleaned // before it is opened, so this resolves inside dist or not at all. func TestPathTraversalCannotEscapeTheBundle(t *testing.T) { for _, path := range []string{ "/../../../../etc/passwd", "/assets/../../../etc/passwd", "/..%2f..%2fetc%2fpasswd", } { rec := get(t, path) if body := rec.Body.String(); strings.Contains(body, "root:") { t.Fatalf("%s leaked a system file", path) } // Falling through to the app is the correct answer: it is not a file. if rec.Code != http.StatusOK { t.Logf("%s -> %d (fine, as long as nothing leaked)", path, rec.Code) } } } // Fingerprinted assets are safe to cache hard - their names change with their // contents - and caching them is most of what makes the app load instantly on // a shop's connection. func TestFingerprintedAssetsAreCachedHard(t *testing.T) { h, err := Handler() if err != nil { t.Fatal(err) } // Find whatever the current build named them. rec := get(t, "/") body := rec.Body.String() i := strings.Index(body, "/assets/") if i < 0 { t.Skip("this build has no fingerprinted assets (placeholder index.html)") } rest := body[i:] name := rest[:strings.IndexAny(rest, "\"'")] got := httptest.NewRecorder() h.ServeHTTP(got, httptest.NewRequest("GET", name, nil)) if got.Code != http.StatusOK { t.Fatalf("%s: got %d", name, got.Code) } if cc := got.Header().Get("Cache-Control"); !strings.Contains(cc, "immutable") { t.Errorf("%s: Cache-Control %q, want immutable", name, cc) } }