Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 37m ago 32290114
history blame raw 7.7 KiB
1package server_test
2
3import (
4 "bytes"
5 "compress/gzip"
6 "io"
7 "net/http"
8 "strconv"
9 "strings"
10 "testing"
11)
12
13// gzipGet fetches a URL as a browser that accepts gzip would. The header is set
14// by hand so Go's transport does not transparently decode the body: the test
15// wants to see what went over the wire.
16func (f *fixture) gzipGet(t *testing.T, path string, headers map[string]string) *http.Response {
17 t.Helper()
18 req, err := http.NewRequest(http.MethodGet, f.http.URL+path, nil)
19 if err != nil {
20 t.Fatal(err)
21 }
22 req.Header.Set("Accept-Encoding", "gzip")
23 for k, v := range headers {
24 req.Header.Set(k, v)
25 }
26 resp, err := (&http.Client{}).Do(req)
27 if err != nil {
28 t.Fatal(err)
29 }
30 t.Cleanup(func() { resp.Body.Close() })
31 return resp
32}
33
34func gunzip(t *testing.T, body io.Reader) string {
35 t.Helper()
36 zr, err := gzip.NewReader(body)
37 if err != nil {
38 t.Fatalf("body is not gzip: %v", err)
39 }
40 raw, err := io.ReadAll(zr)
41 if err != nil {
42 t.Fatalf("read gzip body: %v", err)
43 }
44 return string(raw)
45}
46
47// TestPagesAreCompressedWhenTheClientAsks: a rendered page is mostly markup, and
48// the code browser ships a blob table, so the whole surface is worth gzipping.
49func TestPagesAreCompressedWhenTheClientAsks(t *testing.T) {
50 t.Parallel()
51 f := newFixture(t)
52 f.publish(f.repo)
53
54 resp := f.gzipGet(t, "/"+f.repo+"/code", nil)
55 if resp.StatusCode != http.StatusOK {
56 t.Fatalf("code page = %d", resp.StatusCode)
57 }
58 if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
59 t.Fatalf("Content-Encoding = %q, want gzip", got)
60 }
61 if !strings.Contains(strings.Join(resp.Header.Values("Vary"), ","), "Accept-Encoding") {
62 t.Fatalf("Vary does not name Accept-Encoding: %v", resp.Header.Values("Vary"))
63 }
64 compressed, err := io.ReadAll(resp.Body)
65 if err != nil {
66 t.Fatal(err)
67 }
68 // net/http may add a length for a small buffered response; if it does, it is
69 // the length of the gzipped bytes, never of the page the handler wrote.
70 if got := resp.Header.Get("Content-Length"); got != "" && got != strconv.Itoa(len(compressed)) {
71 t.Fatalf("Content-Length = %q on a %d-byte compressed body", got, len(compressed))
72 }
73 if page := gunzip(t, bytes.NewReader(compressed)); !strings.Contains(page, `id="file-tree"`) {
74 t.Fatalf("decompressed page is not the code browser:\n%s", page[:min(len(page), 300)])
75 }
76}
77
78// TestCompressionIsOptional: a client that does not ask for gzip (curl without
79// --compressed, a Git client, a script reading a header) gets plain bytes.
80func TestCompressionIsOptional(t *testing.T) {
81 t.Parallel()
82 f := newFixture(t)
83 f.publish(f.repo)
84
85 resp := f.gzipGet(t, "/"+f.repo+"/code", map[string]string{"Accept-Encoding": "identity"})
86 if resp.StatusCode != http.StatusOK {
87 t.Fatalf("code page = %d", resp.StatusCode)
88 }
89 if got := resp.Header.Get("Content-Encoding"); got != "" {
90 t.Fatalf("Content-Encoding = %q on an identity response", got)
91 }
92 page, err := io.ReadAll(resp.Body)
93 if err != nil {
94 t.Fatal(err)
95 }
96 if !strings.Contains(string(page), `id="file-tree"`) {
97 t.Fatal("identity page is not the code browser")
98 }
99}
100
101// TestStaticAssetsRevalidate: the Prism bundles are half a megabyte, so a
102// browser must be able to ask "still current?" and be told 304 rather than
103// download them again on every code page.
104func TestStaticAssetsRevalidate(t *testing.T) {
105 t.Parallel()
106 f := newFixture(t)
107
108 resp := f.gzipGet(t, "/-/static/vole.css", map[string]string{"Accept-Encoding": "identity"})
109 if resp.StatusCode != http.StatusOK {
110 t.Fatalf("stylesheet = %d", resp.StatusCode)
111 }
112 etag := resp.Header.Get("ETag")
113 if etag == "" {
114 t.Fatal("stylesheet has no ETag")
115 }
116 if got := resp.Header.Get("Cache-Control"); got != "no-cache" {
117 t.Fatalf("Cache-Control = %q", got)
118 }
119 first, err := io.ReadAll(resp.Body)
120 if err != nil {
121 t.Fatal(err)
122 }
123 if len(first) == 0 {
124 t.Fatal("stylesheet is empty")
125 }
126
127 notModified := f.gzipGet(t, "/-/static/vole.css", map[string]string{
128 "Accept-Encoding": "identity",
129 "If-None-Match": etag,
130 })
131 if notModified.StatusCode != http.StatusNotModified {
132 t.Fatalf("revalidation = %d, want 304", notModified.StatusCode)
133 }
134 if body, _ := io.ReadAll(notModified.Body); len(body) != 0 {
135 t.Fatalf("304 carried %d bytes", len(body))
136 }
137
138 // The compressed representation keeps the same validator, so a cache keys the
139 // two variants by Vary rather than by ETag.
140 compressed := f.gzipGet(t, "/-/static/vole.css", nil)
141 if compressed.Header.Get("ETag") != etag {
142 t.Fatalf("compressed ETag = %q, want %q", compressed.Header.Get("ETag"), etag)
143 }
144 if got := compressed.Header.Get("Content-Encoding"); got != "gzip" {
145 t.Fatalf("compressed stylesheet Content-Encoding = %q", got)
146 }
147 if body := gunzip(t, compressed.Body); body != string(first) {
148 t.Fatal("compressed stylesheet decompresses to something else")
149 }
150}
151
152// TestStaticServesOnlyTheEmbeddedSet: there is no directory to list and no path
153// to walk, so anything not compiled in is a 404.
154func TestStaticServesOnlyTheEmbeddedSet(t *testing.T) {
155 t.Parallel()
156 f := newFixture(t)
157 for _, path := range []string{"/-/static/", "/-/static/nope.js", "/-/static/../go.mod"} {
158 resp := f.gzipGet(t, path, map[string]string{"Accept-Encoding": "identity"})
159 if resp.StatusCode != http.StatusNotFound {
160 t.Fatalf("%s = %d, want 404", path, resp.StatusCode)
161 }
162 }
163}
164
165// TestSiteResponsesAreCompressed: a hosted site is a static file server, the best
166// case gzip has, and it reaches HTTP by a different path than the forge UI (a Host
167// match in server.Handler rather than Web.Middleware). Compressing only the forge
168// would have left every site sending raw HTML and CSS.
169func TestSiteResponsesAreCompressed(t *testing.T) {
170 t.Parallel()
171 f := pagesFixture(t)
172 f.publish(f.repo)
173 f.pushPages(t, map[string]string{
174 "index.html": "<!doctype html><h1>hello site</h1>\n" + strings.Repeat("<p>a paragraph</p>\n", 60),
175 "assets/style.css": "body { color: #123 }\n",
176 })
177 host := f.repo + "." + pagesDomain
178
179 code, body, hdr := f.siteGet(host, "/", [2]string{"Accept-Encoding", "gzip"})
180 if code != http.StatusOK {
181 t.Fatalf("site page = %d", code)
182 }
183 if got := hdr.Get("Content-Encoding"); got != "gzip" {
184 t.Fatalf("Content-Encoding = %q, want gzip", got)
185 }
186 // A length here is the gzipped length, not the site file's.
187 if got := hdr.Get("Content-Length"); got != "" && got != strconv.Itoa(len(body)) {
188 t.Fatalf("Content-Length = %q on a %d-byte compressed body", got, len(body))
189 }
190 if !strings.Contains(strings.Join(hdr.Values("Vary"), ","), "Accept-Encoding") {
191 t.Fatalf("Vary does not name Accept-Encoding: %v", hdr.Values("Vary"))
192 }
193 // The site's own validators survive: a push still invalidates an exact blob sha.
194 if hdr.Get("ETag") == "" || hdr.Get("Last-Modified") == "" {
195 t.Fatalf("missing validators: ETag=%q Last-Modified=%q", hdr.Get("ETag"), hdr.Get("Last-Modified"))
196 }
197 if page := gunzip(t, strings.NewReader(body)); !strings.Contains(page, "hello site") {
198 t.Fatalf("decompressed site page is not the page:\n%s", page[:min(len(page), 200)])
199 }
200
201 // A client that does not ask keeps the byte-for-byte body and its length.
202 code, plain, hdr := f.siteGet(host, "/", [2]string{"Accept-Encoding", "identity"})
203 if code != http.StatusOK || hdr.Get("Content-Encoding") != "" {
204 t.Fatalf("identity site page = %d %q", code, hdr.Get("Content-Encoding"))
205 }
206 if got, want := hdr.Get("Content-Length"), strconv.Itoa(len(plain)); got != want {
207 t.Fatalf("identity Content-Length = %q, want %q", got, want)
208 }
209 etag := hdr.Get("ETag")
210
211 // Revalidation is unaffected by the encoding: the 304 has no body to compress.
212 code, _, hdr = f.siteGet(host, "/", [2]string{"Accept-Encoding", "gzip"}, [2]string{"If-None-Match", etag})
213 if code != http.StatusNotModified || hdr.Get("Content-Encoding") != "" {
214 t.Fatalf("site revalidation = %d %q", code, hdr.Get("Content-Encoding"))
215 }
216}
217