| 1 | package www_test |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "testing/fstest" |
| 11 | |
| 12 | xhtml "golang.org/x/net/html" |
| 13 | |
| 14 | "amadan.net/rastrillo/fiesta" |
| 15 | "amadan.net/rastrillo/fiesta/internal/gallery" |
| 16 | "amadan.net/rastrillo/fiesta/internal/site" |
| 17 | "amadan.net/rastrillo/fiesta/internal/www" |
| 18 | ) |
| 19 | |
| 20 | // siteOf is a site source tree from path to content. |
| 21 | func siteOf(files map[string]string) fstest.MapFS { |
| 22 | m := fstest.MapFS{} |
| 23 | for name, text := range files { |
| 24 | m[name] = &fstest.MapFile{Data: []byte(text)} |
| 25 | } |
| 26 | return m |
| 27 | } |
| 28 | |
| 29 | // pages runs Pages over Fiesta's own tree and siteDir, failed loudly. |
| 30 | // The landing page's template and catalogs are overlaid from the real |
| 31 | // site source, since Pages renders the root page from them. |
| 32 | func pages(t *testing.T, siteDir fstest.MapFS) map[string][]byte { |
| 33 | t.Helper() |
| 34 | out, err := www.Pages(fiesta.FS(), withLanding(t, siteDir)) |
| 35 | if err != nil { |
| 36 | t.Fatalf("Pages: %v", err) |
| 37 | } |
| 38 | return out |
| 39 | } |
| 40 | |
| 41 | // urlsOf reads every href and src on doc the way a browser would, |
| 42 | // whatever the element, so a URL the gallery writes under its mount |
| 43 | // and one a sample wrote are both seen. |
| 44 | func urlsOf(t *testing.T, doc []byte) []string { |
| 45 | t.Helper() |
| 46 | var out []string |
| 47 | z := xhtml.NewTokenizer(bytes.NewReader(doc)) |
| 48 | for { |
| 49 | switch z.Next() { |
| 50 | case xhtml.ErrorToken: |
| 51 | return out |
| 52 | case xhtml.StartTagToken, xhtml.SelfClosingTagToken: |
| 53 | _, more := z.TagName() |
| 54 | for more { |
| 55 | var k, v []byte |
| 56 | k, v, more = z.TagAttr() |
| 57 | if key := string(k); key == "href" || key == "src" { |
| 58 | out = append(out, string(v)) |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | // TestPagesHasARootAndTheDesignSystem: the tree has a landing page at |
| 66 | // the root and the design system's index under design-system/, and |
| 67 | // neither is empty. |
| 68 | func TestPagesHasARootAndTheDesignSystem(t *testing.T) { |
| 69 | out := pages(t, siteOf(nil)) |
| 70 | for _, name := range []string{"index.html", "design-system/index.html"} { |
| 71 | if len(out[name]) == 0 { |
| 72 | t.Errorf("Pages wrote no %s", name) |
| 73 | } |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // TestTheDesignSystemIsMounted: on the design system's index and on |
| 78 | // one example page, every root-relative href and src starts with |
| 79 | // /design-system/, so the gallery served there asks for nothing from |
| 80 | // the site's root. |
| 81 | func TestTheDesignSystemIsMounted(t *testing.T) { |
| 82 | out := pages(t, siteOf(nil)) |
| 83 | for _, name := range []string{"design-system/index.html", "design-system/day/en/button.html"} { |
| 84 | doc, ok := out[name] |
| 85 | if !ok { |
| 86 | t.Errorf("Pages wrote no %s", name) |
| 87 | continue |
| 88 | } |
| 89 | rooted := 0 |
| 90 | for _, u := range urlsOf(t, doc) { |
| 91 | if !strings.HasPrefix(u, "/") { |
| 92 | continue |
| 93 | } |
| 94 | rooted++ |
| 95 | if !strings.HasPrefix(u, www.Mount+"/") { |
| 96 | t.Errorf("%s links %s, which is not under %s/", name, u, www.Mount) |
| 97 | } |
| 98 | } |
| 99 | if rooted == 0 { |
| 100 | t.Errorf("%s has no root-relative URL at all; the check above read nothing", name) |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | // TestTheReleaseBaseIsTheSitesOrigin: the base the design system |
| 106 | // writes its release-file links under is this site's origin and its |
| 107 | // /1/ rewrite, so a link to a shipped file is one of the site's own. |
| 108 | // |
| 109 | // Already passing when written: the constant was declared at its final |
| 110 | // value. Proved live by a named mutation, gallery.ReleaseBase changed |
| 111 | // to end in /2/, which it reported as "is https://fiesta.rastrillo.org/2/, |
| 112 | // want https://fiesta.rastrillo.org/1/". |
| 113 | func TestTheReleaseBaseIsTheSitesOrigin(t *testing.T) { |
| 114 | if gallery.ReleaseBase != www.Origin+"/1/" { |
| 115 | t.Errorf("gallery.ReleaseBase is %q, want %q", gallery.ReleaseBase, www.Origin+"/1/") |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // TestAHiddenSiteFileIsRefused: a hidden file under the site source |
| 120 | // fails the build, and the error names it, rather than being skipped |
| 121 | // or published. |
| 122 | func TestAHiddenSiteFileIsRefused(t *testing.T) { |
| 123 | _, err := www.Pages(fiesta.FS(), siteOf(map[string]string{ |
| 124 | "static/x.css": "body{}\n", |
| 125 | "static/.DS_Store": "\x00", |
| 126 | })) |
| 127 | if err == nil { |
| 128 | t.Fatal("Pages accepted static/.DS_Store") |
| 129 | } |
| 130 | if !strings.Contains(err.Error(), "static/.DS_Store") { |
| 131 | t.Errorf("the error does not name the hidden file: %v", err) |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | // TestStaticFilesAreCopied: a file under static/ in the site source is |
| 136 | // in the tree at the same path, byte for byte. |
| 137 | func TestStaticFilesAreCopied(t *testing.T) { |
| 138 | want := "/* the site's own */\nbody{margin:0}\n" |
| 139 | out := pages(t, siteOf(map[string]string{"static/x.css": want})) |
| 140 | if got := string(out["static/x.css"]); got != want { |
| 141 | t.Errorf("static/x.css is %q, want %q", got, want) |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | // sumsOf is SHA256SUMS as build.Dist writes it: one `<hex> <path>` |
| 146 | // line per file, sorted by path, the sums file itself left out. |
| 147 | func sumsOf(files map[string][]byte) []byte { |
| 148 | names := make([]string, 0, len(files)) |
| 149 | for name := range files { |
| 150 | if name != "SHA256SUMS" { |
| 151 | names = append(names, name) |
| 152 | } |
| 153 | } |
| 154 | sort.Strings(names) |
| 155 | var b bytes.Buffer |
| 156 | for _, name := range names { |
| 157 | sum := sha256.Sum256(files[name]) |
| 158 | b.WriteString(hex.EncodeToString(sum[:]) + " " + name + "\n") |
| 159 | } |
| 160 | return b.Bytes() |
| 161 | } |
| 162 | |
| 163 | // releases is a releases tree holding one small, checksummed 1.0.0. |
| 164 | func releases() fstest.MapFS { |
| 165 | files := map[string][]byte{ |
| 166 | "fiesta.css": []byte("/*! Fiesta 1.0.0 */\nbody{margin:0}\n"), |
| 167 | "LICENSE": []byte("MPL-2.0\n"), |
| 168 | } |
| 169 | files["SHA256SUMS"] = sumsOf(files) |
| 170 | m := fstest.MapFS{} |
| 171 | for name, data := range files { |
| 172 | m["1.0.0/"+name] = &fstest.MapFile{Data: data} |
| 173 | } |
| 174 | return m |
| 175 | } |
| 176 | |
| 177 | // TestSiteBuildAcceptsThePages is a preservation test: site.Build |
| 178 | // takes FS(Pages(...)) as its pages beside a checksummed 1.0.0/, and |
| 179 | // what Pages wrote is in the site at the same paths beside the release. |
| 180 | func TestSiteBuildAcceptsThePages(t *testing.T) { |
| 181 | out := pages(t, siteOf(map[string]string{"static/x.css": "body{}\n"})) |
| 182 | built, err := site.Build(releases(), www.FS(out), site.Rules{}) |
| 183 | if err != nil { |
| 184 | t.Fatalf("site.Build refused the pages: %v", err) |
| 185 | } |
| 186 | for name, data := range out { |
| 187 | if !bytes.Equal(built[name], data) { |
| 188 | t.Errorf("%s in the site differs from Pages, or is missing", name) |
| 189 | } |
| 190 | } |
| 191 | if _, ok := built["1.0.0/fiesta.css"]; !ok { |
| 192 | t.Error("the site lost the release") |
| 193 | } |
| 194 | if got := string(built["_redirects"]); got != "/1/* /1.0.0/:splat 200\n" { |
| 195 | t.Errorf("_redirects is %q", got) |
| 196 | } |
| 197 | } |
| 198 | |