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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

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

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 2h ago 4b8c9e1d
history blame raw 7.6 KiB
1package compress
2
3import (
4 "bytes"
5 "compress/gzip"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11)
12
13// serveWithCompression runs one handler through the compressor the middleware
14// installs and returns the recorder it wrote to.
15func serveWithCompression(t *testing.T, r *http.Request, handler http.HandlerFunc) *httptest.ResponseRecorder {
16 t.Helper()
17 rec := httptest.NewRecorder()
18 c := New(rec, r)
19 handler(c, r)
20 c.Close()
21 return rec
22}
23
24func TestCompressionNegotiation(t *testing.T) {
25 t.Parallel()
26 body := strings.Repeat("vole compresses text responses. ", 100)
27
28 cases := []struct {
29 name string
30 method string
31 accept string
32 rangeHd string
33 status int
34 ctype string
35 length string
36 cache string
37 encoded string
38 want bool
39 }{
40 {name: "gzip asked for", accept: "gzip", status: 200, ctype: "text/html; charset=utf-8", want: true},
41 {name: "gzip with q", accept: "gzip;q=0.5, identity", status: 200, ctype: "text/html", want: true},
42 {name: "no accept-encoding", accept: "", status: 200, ctype: "text/html", want: false},
43 {name: "gzip refused", accept: "gzip;q=0", status: 200, ctype: "text/html", want: false},
44 {name: "identity preferred", accept: "identity, gzip;q=0", status: 200, ctype: "text/html", want: false},
45 {name: "other encoding only", accept: "br, zstd", status: 200, ctype: "text/html", want: false},
46 {name: "head", method: http.MethodHead, accept: "gzip", status: 200, ctype: "text/html", want: false},
47 {name: "range request", accept: "gzip", rangeHd: "bytes=0-99", status: 200, ctype: "text/plain", want: false},
48 {name: "png", accept: "gzip", status: 200, ctype: "image/png", want: false},
49 {name: "svg", accept: "gzip", status: 200, ctype: "image/svg+xml", want: true},
50 {name: "json", accept: "gzip", status: 200, ctype: "application/json; charset=utf-8", want: true},
51 {name: "javascript", accept: "gzip", status: 200, ctype: "text/javascript; charset=utf-8", want: true},
52 {name: "octet stream", accept: "gzip", status: 200, ctype: "application/octet-stream", want: false},
53 {name: "event stream", accept: "gzip", status: 200, ctype: "text/event-stream", want: false},
54 {name: "already encoded", accept: "gzip", status: 200, ctype: "text/html", encoded: "br", want: false},
55 {name: "not modified", accept: "gzip", status: http.StatusNotModified, ctype: "text/html", want: false},
56 {name: "partial content", accept: "gzip", status: http.StatusPartialContent, ctype: "text/plain", want: false},
57 {name: "redirect", accept: "gzip", status: http.StatusFound, ctype: "text/html", want: false},
58 {name: "not found page", accept: "gzip", status: http.StatusNotFound, ctype: "text/html; charset=utf-8", want: true},
59 {name: "server error page", accept: "gzip", status: http.StatusInternalServerError, ctype: "text/html", want: true},
60 {name: "no content", accept: "gzip", status: http.StatusNoContent, ctype: "text/html", want: false},
61 {name: "created", accept: "gzip", status: http.StatusCreated, ctype: "text/html", want: true},
62 {name: "small body", accept: "gzip", status: 200, ctype: "text/html", length: "100", want: false},
63 {name: "large body", accept: "gzip", status: 200, ctype: "text/html", length: "5000", want: true},
64 {name: "no-transform", accept: "gzip", status: 200, ctype: "text/plain", cache: "no-transform", want: false},
65 }
66 for _, c := range cases {
67 t.Run(c.name, func(t *testing.T) {
68 method := c.method
69 if method == "" {
70 method = http.MethodGet
71 }
72 r := httptest.NewRequest(method, "/", nil)
73 if c.accept != "" {
74 r.Header.Set("Accept-Encoding", c.accept)
75 }
76 if c.rangeHd != "" {
77 r.Header.Set("Range", c.rangeHd)
78 }
79 h := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
80 if c.ctype != "" {
81 rw.Header().Set("Content-Type", c.ctype)
82 }
83 if c.length != "" {
84 rw.Header().Set("Content-Length", c.length)
85 }
86 if c.cache != "" {
87 rw.Header().Set("Cache-Control", c.cache)
88 }
89 if c.encoded != "" {
90 rw.Header().Set("Content-Encoding", c.encoded)
91 }
92 rw.WriteHeader(c.status)
93 if method != http.MethodHead && c.status != http.StatusNoContent && c.status != http.StatusNotModified && c.status != http.StatusFound {
94 _, _ = io.WriteString(rw, body)
95 }
96 })
97 rec := serveWithCompression(t, r, h)
98
99 gotEncoding := rec.Header().Get("Content-Encoding")
100 if c.want && gotEncoding != "gzip" {
101 t.Fatalf("Content-Encoding = %q, want gzip", gotEncoding)
102 }
103 if !c.want && gotEncoding == "gzip" {
104 t.Fatalf("response was compressed but should not be")
105 }
106 if c.want {
107 if !strings.Contains(strings.Join(rec.Header().Values("Vary"), ","), "Accept-Encoding") {
108 t.Fatalf("Vary does not name Accept-Encoding: %v", rec.Header().Values("Vary"))
109 }
110 if got := rec.Header().Get("Content-Length"); got != "" {
111 t.Fatalf("Content-Length = %q on a compressed body", got)
112 }
113 zr, err := gzip.NewReader(bytes.NewReader(rec.Body.Bytes()))
114 if err != nil {
115 t.Fatalf("body is not gzip: %v", err)
116 }
117 decoded, err := io.ReadAll(zr)
118 if err != nil {
119 t.Fatalf("read gzip body: %v", err)
120 }
121 if string(decoded) != body {
122 t.Fatal("decompressed body differs from what the handler wrote")
123 }
124 return
125 }
126 bodiless := method == http.MethodHead || c.status == http.StatusNoContent || c.status == http.StatusNotModified || c.status == http.StatusFound
127 if bodiless && rec.Body.Len() != 0 {
128 t.Fatalf("response carries a %d-byte body it must not have", rec.Body.Len())
129 }
130 if !bodiless && rec.Body.String() != body {
131 t.Fatalf("body was altered: %d bytes", rec.Body.Len())
132 }
133 if got := rec.Header().Get("Vary"); strings.Contains(got, "Accept-Encoding") {
134 t.Fatalf("Vary names Accept-Encoding on an identity response: %q", got)
135 }
136 })
137 }
138}
139
140// TestCompressionFlushes: a handler that streams (a log, a git side-band
141// response) must reach the client as it writes, not when it finishes.
142func TestCompressionFlushes(t *testing.T) {
143 t.Parallel()
144 r := httptest.NewRequest(http.MethodGet, "/", nil)
145 r.Header.Set("Accept-Encoding", "gzip")
146 rec := httptest.NewRecorder()
147 c := New(rec, r)
148 handler := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
149 rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
150 _, _ = io.WriteString(rw, "first\n")
151 rw.(http.Flusher).Flush()
152 if !rec.Flushed {
153 t.Error("compressor did not flush the underlying writer")
154 }
155 _, _ = io.WriteString(rw, "second\n")
156 })
157 handler(c, r)
158 c.Close()
159 zr, err := gzip.NewReader(bytes.NewReader(rec.Body.Bytes()))
160 if err != nil {
161 t.Fatalf("body is not gzip: %v", err)
162 }
163 decoded, err := io.ReadAll(zr)
164 if err != nil {
165 t.Fatalf("read gzip body: %v", err)
166 }
167 if string(decoded) != "first\nsecond\n" {
168 t.Fatalf("streamed body = %q", decoded)
169 }
170}
171
172// TestWriterUnwraps: http.ResponseController reaches the real writer
173// through Unwrap, so nothing in the chain needs its own accessor.
174func TestWriterUnwraps(t *testing.T) {
175 t.Parallel()
176 r := httptest.NewRequest(http.MethodGet, "/", nil)
177 rec := httptest.NewRecorder()
178 // One transparent wrapper, as the forge UI puts a status recorder between the
179 // handler and the compressor.
180 inner := wrapped{rec}
181 c := New(inner, r)
182 if got := c.Unwrap(); got != http.ResponseWriter(inner) {
183 t.Fatalf("Unwrap = %T, want the wrapped writer", got)
184 }
185 // The controller finds the recorder's Flush only if Unwrap is honoured.
186 if err := http.NewResponseController(c).Flush(); err != nil {
187 t.Fatalf("ResponseController.Flush through the compressor: %v", err)
188 }
189}
190
191// wrapped is the shape of every writer between a handler and the compressor:
192// a ResponseWriter that forwards Header, Write and WriteHeader.
193type wrapped struct{ http.ResponseWriter }
194