package www_test import ( "bufio" "net/http" "net/http/httptest" "os" "path" "strings" "testing" xhtml "golang.org/x/net/html" ) // lines reads a testdata file: one entry a line, # comments and blank // lines skipped. func lines(t *testing.T, name string) []string { t.Helper() f, err := os.Open("testdata/" + name) if err != nil { t.Fatal(err) } defer f.Close() var out []string sc := bufio.NewScanner(f) for sc.Scan() { if l := strings.TrimSpace(sc.Text()); l != "" && !strings.HasPrefix(l, "#") { out = append(out, l) } } if err := sc.Err(); err != nil { t.Fatal(err) } if len(out) == 0 { t.Fatalf("testdata/%s lists nothing", name) } return out } // part3Home is where a part 3 page lives now, as a URL: a theme's page // under its English tree, a right-to-left twin under its Arabic one, // same name, and an index as its directory, which is the address the // edge serves an index at (it sends .../index.html to ./, so a // Location naming the file would be a second hop). func part3Home(old string) string { rest, ok := strings.CutPrefix(old, "design-system/") if !ok { return "" } theme, rest, ok := strings.Cut(rest, "/") if !ok { return "/" + old // the design system's own index, which stayed } locale := "en" if r, ok := strings.CutPrefix(rest, "rtl/"); ok { locale, rest = "ar", r } to := "/design-system/" + theme + "/" + locale + "/" + rest if d, ok := strings.CutSuffix(to, "/index.html"); ok { to = d + "/" } return to } // serve answers one GET through the test server's handler. func serve(h http.Handler, target string) *httptest.ResponseRecorder { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil)) return rec } // TestEveryPart3AddressStillReachesAPage: every page part 3 published // under /design-system/ (testdata/part3-addresses.txt), and an index's // directory address too, answers either with the page itself, where // the path is still a page, or with a 301 whose Location is that // page's part 4 address and answers 200 with HTML: one hop, never two. // The Location carries no fragment, so the browser keeps the one the // old link had; and on a sample of pages // (testdata/part3-ids.txt) the page it lands on has every id part 3's // page had, so the fragment still finds its place. func TestEveryPart3AddressStillReachesAPage(t *testing.T) { tree := builtSite(t) h := newSiteHandler(t, tree) redirected, stayed := 0, 0 for _, old := range lines(t, "part3-addresses.txt") { want := part3Home(old) if want == "" { t.Errorf("%s is not under design-system/; the inventory is part 3's design system", old) continue } asked := []string{"/" + old} if d, ok := strings.CutSuffix(old, "/index.html"); ok { asked = append(asked, "/"+d+"/") } for _, a := range asked { res := serve(h, a) if _, ok := tree[old]; ok { if res.Code != http.StatusOK { t.Errorf("%s is still a page and answers %d", a, res.Code) } stayed++ continue } loc := res.Header().Get("Location") if res.Code != http.StatusMovedPermanently || loc != want { t.Errorf("%s answers %d, Location %q; want 301 to %s", a, res.Code, loc, want) continue } if strings.Contains(loc, "#") { t.Errorf("%s: the Location %s has a fragment, which replaces the one the link had", a, loc) } there := serve(h, loc) if there.Code != http.StatusOK || !strings.HasPrefix(there.Header().Get("Content-Type"), "text/html") { t.Errorf("%s redirects to %s, which answers %d %s; one hop must reach the page", a, loc, there.Code, there.Header().Get("Content-Type")) } redirected++ } } t.Logf("%d part 3 addresses redirect to their pages in one hop, %d are still pages", redirected, stayed) for _, l := range lines(t, "part3-ids.txt") { f := strings.Fields(l) old, ids := f[0], f[1:] loc := serve(h, "/"+old).Header().Get("Location") if loc == "" { t.Errorf("%s does not redirect, so its ids cannot be compared", old) continue } res := serve(h, loc) if res.Code != http.StatusOK { t.Errorf("%s: %s answers %d", old, loc, res.Code) continue } have := map[string]bool{} walk(parse(t, res.Body.Bytes()), func(n *xhtml.Node) { if id := attr(n, "id"); id != "" { have[id] = true } }) for _, id := range ids { if !have[id] { t.Errorf("%s#%s lands on %s#%s, and that page has no element with that id", old, id, loc, id) } } } } // TestNoRedirectShadowsAPage: no rule in the site's _redirects matches // the path of anything the site serves, a file or an index's directory, // with or without its slash; and none is forced. The edge serves a file // before any rule not forced, so a rule that matched a page would be // dead today, but it is proved here not to match at all, so no page's // address depends on that order. Every page then answers 200 through // the test server. func TestNoRedirectShadowsAPage(t *testing.T) { tree := builtSite(t) rules, err := parseRedirects(tree["_redirects"]) if err != nil { t.Fatalf("_redirects: %v", err) } if len(rules) < 2 { t.Fatalf("_redirects holds %d rules; the release rewrite and the design system's moved pages are at least two", len(rules)) } for _, r := range rules { if r.force { t.Errorf("_redirects line %d is forced, so it would answer for a file", r.line) } } h := newSiteHandler(t, tree) for name := range tree { if name == "_redirects" || name == "_headers" { continue } paths := []string{"/" + name} if d, ok := strings.CutSuffix("/"+name, "/index.html"); ok { paths = append(paths, d+"/") } for _, p := range paths { for _, r := range rules { if _, ok := r.match("", segments(path.Clean(p))); ok { t.Errorf("_redirects line %d matches %s, which the site serves", r.line, p) } } } if res := serve(h, "/"+name); res.Code != http.StatusOK { t.Errorf("/%s answers %d", name, res.Code) } } } // TestNoRedirectLeadsToAnotherRule: no redirect's destination, whatever // its placeholders are given, can match the source of any rule, so no // address, part 3's or any other, is redirected twice or in a loop. // A placeholder in a destination stands for any one segment and :splat // for any number, so this holds for every request, not only the // addresses the inventory lists. func TestNoRedirectLeadsToAnotherRule(t *testing.T) { rules, err := parseRedirects(builtSite(t)["_redirects"]) if err != nil { t.Fatalf("_redirects: %v", err) } for _, from := range rules { if !isRedirect(from.status) || from.prefix != "" { continue } dest := segments(path.Clean("/" + from.to)) for _, to := range rules { if overlaps(dest, to) { t.Errorf("_redirects line %d redirects to %s, which line %d can match: a second hop", from.line, from.to, to.line) } } } } // overlaps reports whether some path fits both dest, a destination's // segments in which a segment holding a placeholder stands for any // segment and a final :splat for any number of them, and the source // of r. func overlaps(dest []string, r rule) bool { wild := func(seg string) bool { return strings.Contains(seg, ":") } tail := len(dest) > 0 && dest[len(dest)-1] == ":splat" fixed := dest if tail { fixed = dest[:len(dest)-1] } // Where both have a segment, both must be able to hold the same one. n := min(len(fixed), len(r.segs)) for i := 0; i < n; i++ { if !wild(fixed[i]) && !strings.HasPrefix(r.segs[i], ":") && fixed[i] != r.segs[i] { return false } } switch { case len(fixed) == len(r.segs): return true case len(fixed) > len(r.segs): return r.splat default: // The source wants more segments than dest fixes: only a // splat in dest can supply them. return tail } } // TestTheRedirectsFileIsWithinCarlosLimits: the site's _redirects is // one `carlos ship` would pack: every line parses under the edge's // grammar, and it holds no more than 2,000 rules, 256 KiB and 2,048 // bytes a line. carlos ship has no dry run for a static tree, so the // packer's parse is mirrored in redirects_test.go. func TestTheRedirectsFileIsWithinCarlosLimits(t *testing.T) { data := builtSite(t)["_redirects"] rules, err := parseRedirects(data) if err != nil { t.Errorf("carlos ship would refuse _redirects: %v", err) } if len(rules) > redirectsMaxRules { t.Errorf("_redirects holds %d rules; the edge accepts %d", len(rules), redirectsMaxRules) } if len(data) > redirectsMaxBytes { t.Errorf("_redirects is %d bytes; the edge accepts %d", len(data), redirectsMaxBytes) } longest := 0 for i, l := range strings.Split(string(data), "\n") { longest = max(longest, len(l)) if len(l) > redirectsMaxLine { t.Errorf("_redirects line %d is %d bytes; the edge accepts %d", i+1, len(l), redirectsMaxLine) } } t.Logf("_redirects: %d rules, %d bytes, longest line %d bytes", len(rules), len(data), longest) }