rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "io"
8 "io/fs"
9 "maps"
10 "os"
11 "path/filepath"
12 "regexp"
13 "sort"
14 "strings"
15 "testing"
16
17 "amadan.net/rastrillo/fiesta"
18 "amadan.net/rastrillo/fiesta/internal/site"
19)
20
21// releaseRule is a release's path line in _headers and the
22// Cache-Control under it.
23var releaseRule = regexp.MustCompile(`(?m)^/([0-9]+\.[0-9]+\.[0-9]+)/\*\n Cache-Control: (.*)$`)
24
25// releaseCaches reads the Cache-Control _headers gives each release,
26// by version, from a site fiesta site wrote under out.
27func releaseCaches(t *testing.T, out string) map[string]string {
28 t.Helper()
29 data, err := os.ReadFile(filepath.Join(out, shipDir, "_headers"))
30 if err != nil {
31 t.Fatal(err)
32 }
33 caches := map[string]string{}
34 for _, m := range releaseRule.FindAllStringSubmatch(string(data), -1) {
35 caches[m[1]] = m[2]
36 }
37 return caches
38}
39
40// copyRelease copies the committed release version, every file and
41// the stamp, into releases/version.
42func copyRelease(t *testing.T, version, releases string) {
43 t.Helper()
44 src := filepath.Join("..", "..", "releases", version)
45 err := filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error {
46 if err != nil || d.IsDir() {
47 return err
48 }
49 rel, err := filepath.Rel(src, p)
50 if err != nil {
51 return err
52 }
53 data, err := os.ReadFile(p)
54 if err != nil {
55 return err
56 }
57 dest := filepath.Join(releases, version, rel)
58 if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
59 return err
60 }
61 return os.WriteFile(dest, data, 0o644)
62 })
63 if err != nil {
64 t.Fatal(err)
65 }
66}
67
68// resum rewrites dir's SHA256SUMS from the files beside it, as fiesta
69// release writes it, so a changed release still passes site.Release
70// and only its tag can tell it apart.
71func resum(t *testing.T, dir string) {
72 t.Helper()
73 var names []string
74 err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
75 if err != nil || d.IsDir() {
76 return err
77 }
78 rel, err := filepath.Rel(dir, p)
79 if err != nil {
80 return err
81 }
82 if rel = filepath.ToSlash(rel); rel != "SHA256SUMS" && rel != site.StampName {
83 names = append(names, rel)
84 }
85 return nil
86 })
87 if err != nil {
88 t.Fatal(err)
89 }
90 sort.Strings(names)
91 var sums bytes.Buffer
92 for _, name := range names {
93 data, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(name)))
94 if err != nil {
95 t.Fatal(err)
96 }
97 sum := sha256.Sum256(data)
98 sums.WriteString(hex.EncodeToString(sum[:]) + " " + name + "\n")
99 }
100 if err := os.WriteFile(filepath.Join(dir, "SHA256SUMS"), sums.Bytes(), 0o644); err != nil {
101 t.Fatal(err)
102 }
103}
104
105// TestSitePromisesAYearOnlyToTaggedReleases: fiesta site keeps every
106// release five minutes, as /M/, unless -published names it, and a
107// release named there must be its tag's in the -git repository, file
108// for file, or nothing is written. A candidate shipped, rebuilt and
109// shipped again would otherwise leave browsers holding its first bytes
110// for a year at the same address.
111//
112// The repository holds the committed 1.0.0 and a candidate of the
113// current version. Refused, each by name and before anything is
114// written: -published without -git, -git without -published, a
115// version that is no X.Y.Z, a release with no tag, and a tagged release
116// with a file changed, one added, one taken away and one made a link
117// after the tag, its sums rewritten to agree; a tag holding no such
118// release; and a tagged release missing from the tree. Published once
119// its tag holds it, 1.0.0 gets a year and the candidate still five
120// minutes.
121func TestSitePromisesAYearOnlyToTaggedReleases(t *testing.T) {
122 repo := scratchRepo(t)
123 releases := filepath.Join(repo, "releases")
124 copyRelease(t, "1.0.0", releases)
125 if err := fiestaRelease(t, releases, fiesta.Version, false); err != nil {
126 t.Fatal(err)
127 }
128 siteDir := filepath.Join(t.TempDir(), "site")
129 siteFixture(t, siteDir)
130 git(t, repo, "add", "releases")
131 git(t, repo, "commit", "-q", "-m", "releases")
132 out := filepath.Join(t.TempDir(), "site")
133 siteWith := func(extra ...string) error {
134 t.Helper()
135 return run(append([]string{"site", "-releases", releases, "-site", siteDir, "-out", out}, extra...), io.Discard)
136 }
137
138 if err := siteWith(); err != nil {
139 t.Fatalf("fiesta site with nothing published: %v", err)
140 }
141 brief := map[string]string{"1.0.0": "public, max-age=300", fiesta.Version: "public, max-age=300"}
142 if got := releaseCaches(t, out); !maps.Equal(got, brief) {
143 t.Errorf("with nothing published, _headers caches the releases %v; want %v", got, brief)
144 }
145 written, err := os.ReadFile(filepath.Join(out, shipDir, "_headers"))
146 if err != nil {
147 t.Fatal(err)
148 }
149 // unwritten fails if a refused run touched the site on disk.
150 unwritten := func(what string) {
151 t.Helper()
152 if got, err := os.ReadFile(filepath.Join(out, shipDir, "_headers")); err != nil || !bytes.Equal(got, written) {
153 t.Errorf("fiesta site refused %s but rewrote the site: %v", what, err)
154 }
155 }
156
157 for _, c := range []struct {
158 what string
159 args []string
160 want []string
161 }{
162 {"-published without -git", []string{"-published", "1.0.0"}, []string{"-published needs -git"}},
163 {"-git without -published", []string{"-git", repo}, []string{"-git is only for -published"}},
164 {"a version that is no X.Y.Z", []string{"-published", "1.0", "-git", repo}, []string{`"1.0" is not a version`}},
165 {"a release with no tag", []string{"-published", "1.0.0", "-git", repo}, []string{"no tag v1.0.0"}},
166 {"a candidate", []string{"-published", fiesta.Version, "-git", repo}, []string{"no tag v" + fiesta.Version}},
167 } {
168 err := siteWith(c.args...)
169 if err == nil {
170 t.Errorf("fiesta site accepted %s", c.what)
171 continue
172 }
173 for _, want := range c.want {
174 if !strings.Contains(err.Error(), want) {
175 t.Errorf("the refusal of %s does not say %q: %v", c.what, want, err)
176 }
177 }
178 unwritten(c.what)
179 }
180
181 git(t, repo, "tag", "-a", "-m", "Fiesta 1.0.0", "v1.0.0")
182 one := filepath.Join(releases, "1.0.0")
183 licence := filepath.Join(one, "LICENSE")
184 data, err := os.ReadFile(licence)
185 if err != nil {
186 t.Fatal(err)
187 }
188 if err := os.WriteFile(licence, append(data, "\nchanged after the tag\n"...), 0o644); err != nil {
189 t.Fatal(err)
190 }
191 if err := os.WriteFile(filepath.Join(one, "extra.txt"), []byte("added after the tag\n"), 0o644); err != nil {
192 t.Fatal(err)
193 }
194 if err := os.Remove(filepath.Join(one, "busy.js")); err != nil {
195 t.Fatal(err)
196 }
197 // A link to the very bytes the tag holds is still not the tag's
198 // file: what it points at can change after the check.
199 notice := filepath.Join(one, "NOTICE")
200 noticeBytes, err := os.ReadFile(notice)
201 if err != nil {
202 t.Fatal(err)
203 }
204 elsewhere := filepath.Join(t.TempDir(), "NOTICE")
205 if err := os.WriteFile(elsewhere, noticeBytes, 0o644); err != nil {
206 t.Fatal(err)
207 }
208 if err := os.Remove(notice); err != nil {
209 t.Fatal(err)
210 }
211 if err := os.Symlink(elsewhere, notice); err != nil {
212 t.Fatal(err)
213 }
214 resum(t, one)
215 err = siteWith("-published", "1.0.0", "-git", repo)
216 if err == nil {
217 t.Error("fiesta site published a release whose bytes differ from its tag")
218 } else {
219 for _, want := range []string{"LICENSE differs from tag v1.0.0", "SHA256SUMS differs from tag v1.0.0",
220 "extra.txt is not in tag v1.0.0", "busy.js is in tag v1.0.0 and missing", "NOTICE is not a regular file"} {
221 if !strings.Contains(err.Error(), want) {
222 t.Errorf("the refusal does not say %q: %v", want, err)
223 }
224 }
225 }
226 unwritten("a release changed after its tag")
227
228 if err := os.Remove(notice); err != nil {
229 t.Fatal(err)
230 }
231 git(t, repo, "checkout", "-q", "--", "releases")
232 if err := os.Remove(filepath.Join(one, "extra.txt")); err != nil {
233 t.Fatal(err)
234 }
235
236 // A tag that holds no such release, and a tagged release taken out
237 // of the tree, are refused as well.
238 git(t, repo, "tag", "-a", "-m", "not a release", "v2.0.0")
239 aside := filepath.Join(t.TempDir(), "1.0.0")
240 if err := os.Rename(one, aside); err != nil {
241 t.Fatal(err)
242 }
243 for _, c := range []struct{ version, want string }{
244 {"2.0.0", "tag v2.0.0 holds no releases/2.0.0/"},
245 {"1.0.0", "v1.0.0 is tagged but " + one + " is missing"},
246 } {
247 err := siteWith("-published", c.version, "-git", repo)
248 if err == nil || !strings.Contains(err.Error(), c.want) {
249 t.Errorf("fiesta site -published %s: %v; want a refusal saying %q", c.version, err, c.want)
250 }
251 unwritten("-published " + c.version)
252 }
253 if err := os.Rename(aside, one); err != nil {
254 t.Fatal(err)
255 }
256 if err := siteWith("-published", "1.0.0", "-git", repo); err != nil {
257 t.Fatalf("fiesta site refused 1.0.0, which its tag holds: %v", err)
258 }
259 want := map[string]string{"1.0.0": "public, max-age=31536000, immutable", fiesta.Version: "public, max-age=300"}
260 if got := releaseCaches(t, out); !maps.Equal(got, want) {
261 t.Errorf("with 1.0.0 published, _headers caches the releases %v; want %v", got, want)
262 }
263}
264