blossom_test.mx raw
1 // Package blossom tests cover the pure request-routing surface: the hex/extension
2 // tables, the upload handler and the path gate. The HTTP transport itself lives
3 // in pkg/relay/wire and is out of scope here.
4 //
5 // A former compiler defect is fixed and the cases it hid are asserted again:
6 // `range` over a string loaded its element as a 4-byte word (the element type
7 // defaulted to i32 and was narrowed only for a slice), which made isHex reject
8 // every real hash - every blob GET/HEAD answered 404 - and made
9 // net/url.validOptionalPort reject a valid `host:port`, so an upstream with a
10 // port read as self. stage4 now loads a byte and zero-extends it (see the
11 // commit "one write per coverage line, and a byte-sized range over a string").
12 package blossom
13
14 import (
15 "os"
16 "testing"
17 )
18
19 // blTmp creates a server over a fresh directory. The caller owns both.
20 func blTmp(t *testing.T) (s *Server, dir string) {
21 t.Helper()
22 dir, derr := os.MkdirTemp("", "moxie-blossom")
23 if derr != nil {
24 t.Fatal(derr)
25 }
26 s, serr := New(dir)
27 if serr != nil {
28 t.Fatal(serr)
29 }
30 return s, dir
31 }
32
33 // TestIsHex pins the hex test every blob path depends on.
34 func TestIsHex(t *testing.T) {
35 if !isHex("") {
36 t.Fatal("isHex(\"\") must be vacuously true")
37 }
38 if !isHex("a") {
39 t.Fatal("single hex letter rejected")
40 }
41 if !isHex("0") {
42 t.Fatal("single digit rejected")
43 }
44 if isHex("g") {
45 t.Fatal("single non-hex letter accepted")
46 }
47 if !isHex("0123456789") {
48 t.Fatal("digit string rejected")
49 }
50 if !isHex("abcdefABCDEF") {
51 t.Fatal("mixed-case hex string rejected")
52 }
53 if !isHex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") {
54 t.Fatal("a 64-character hash rejected")
55 }
56 if isHex("0123456789abcdefg") {
57 t.Fatal("a non-hex character accepted")
58 }
59 }
60
61 // TestMimeFromExt pins the extension to content-type table and the binary
62 // fallback.
63 func TestMimeFromExt(t *testing.T) {
64 cases := []struct {
65 ext string
66 want string
67 }{
68 {"png", "image/png"},
69 {"jpg", "image/jpeg"},
70 {"jpeg", "image/jpeg"},
71 {"gif", "image/gif"},
72 {"webp", "image/webp"},
73 {"svg", "image/svg+xml"},
74 {"pdf", "application/pdf"},
75 {"mp4", "video/mp4"},
76 {"webm", "video/webm"},
77 {"mov", "video/quicktime"},
78 {"mkv", "video/x-matroska"},
79 {"avi", "video/x-msvideo"},
80 {"mp3", "audio/mpeg"},
81 {"ogg", "audio/ogg"},
82 {"m4a", "audio/mp4"},
83 {"", "application/octet-stream"},
84 {"txt", "application/octet-stream"},
85 {"PNG", "application/octet-stream"},
86 }
87 for _, c := range cases {
88 got := mimeFromExt(c.ext)
89 if got != c.want {
90 t.Fatalf("mimeFromExt(%q) = %q, want %q", c.ext, got, c.want)
91 }
92 }
93 }
94
95 // TestExtFromMime pins the reverse table and that an unknown type yields no
96 // extension (so the upload URL is just the hash).
97 func TestExtFromMime(t *testing.T) {
98 cases := []struct {
99 mime string
100 want string
101 }{
102 {"image/png", ".png"},
103 {"image/jpeg", ".jpg"},
104 {"image/jpg", ".jpg"},
105 {"image/gif", ".gif"},
106 {"image/webp", ".webp"},
107 {"image/svg+xml", ".svg"},
108 {"video/mp4", ".mp4"},
109 {"video/webm", ".webm"},
110 {"video/quicktime", ".mov"},
111 {"video/x-matroska", ".mkv"},
112 {"video/x-msvideo", ".avi"},
113 {"audio/mpeg", ".mp3"},
114 {"audio/ogg", ".ogg"},
115 {"audio/mp4", ".m4a"},
116 {"text/plain", ""},
117 {"", ""},
118 }
119 for _, c := range cases {
120 got := extFromMime(c.mime)
121 if got != c.want {
122 t.Fatalf("extFromMime(%q) = %q, want %q", c.mime, got, c.want)
123 }
124 }
125 }
126
127 // TestIsSelfUpstream pins the proxy loop guard: only a URL with a parseable
128 // host is treated as a real upstream. The host:port case is omitted because
129 // net/url.validOptionalPort hits the same range-over-string defect and reports
130 // a valid port as invalid (see the file-head defect).
131 func TestIsSelfUpstream(t *testing.T) {
132 if !isSelfUpstream("") {
133 t.Fatal("empty upstream must read as self")
134 }
135 if !isSelfUpstream("not a url") {
136 t.Fatal("unparseable upstream must read as self")
137 }
138 if !isSelfUpstream("/relative/path") {
139 t.Fatal("hostless relative upstream must read as self")
140 }
141 if isSelfUpstream("https://relay.example.com") {
142 t.Fatal("absolute upstream must not read as self")
143 }
144 if isSelfUpstream("http://relay.example.com:8080/blossom/") {
145 t.Fatal("an absolute upstream with a port read as self")
146 }
147 if isSelfUpstream("https://relay.example.com:443") {
148 t.Fatal("an https upstream with a port read as self")
149 }
150 }
151
152 // TestHandleRawPathValidation pins the path gate: OPTIONS is answered without
153 // touching disk, and a GET whose hash is not exactly 64 hex characters is a
154 // 404 with CORS headers.
155 func TestHandleRawPathValidation(t *testing.T) {
156 s, dir := blTmp(t)
157 defer os.RemoveAll(dir)
158
159 status, headers, body := s.HandleRawWithUpstream("OPTIONS", "/anything", nil, nil, "")
160 if status != 204 {
161 t.Fatalf("OPTIONS status = %d, want 204", status)
162 }
163 if body != nil {
164 t.Fatal("OPTIONS must have no body")
165 }
166 if headers["Access-Control-Allow-Origin"] != "*" {
167 t.Fatal("OPTIONS must carry CORS headers")
168 }
169
170 short := "0123456789abcdef"
171 status2, _, _ := s.HandleRawWithUpstream("GET", "/"|short, nil, nil, "")
172 if status2 != 404 {
173 t.Fatalf("short hash status = %d, want 404", status2)
174 }
175
176 nonHex := ""
177 for i := 0; i < 64; i++ {
178 nonHex = nonHex | "z"
179 }
180 status3, _, _ := s.HandleRawWithUpstream("GET", "/"|nonHex, nil, nil, "")
181 if status3 != 404 {
182 t.Fatalf("non-hex hash status = %d, want 404", status3)
183 }
184
185 // A dot at position 0 leaves a four-character hash and must 404.
186 status4, _, _ := s.HandleRawWithUpstream("GET", "/.png", nil, nil, "")
187 if status4 != 404 {
188 t.Fatalf("dot-only path status = %d, want 404", status4)
189 }
190
191 // 64 hex chars, local miss, no upstream: a clean 404 with CORS headers.
192 miss := "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"
193 status5, h5, _ := s.HandleRawWithUpstream("GET", "/"|miss, nil, nil, "")
194 if status5 != 404 {
195 t.Fatalf("missing blob status = %d, want 404", status5)
196 }
197 if h5["Access-Control-Allow-Origin"] != "*" {
198 t.Fatal("404 must carry CORS headers")
199 }
200 }
201
202 // TestHandleUpload pins the upload contract: empty body is 400, a real body is
203 // written under the sha256 of its content and answered with that hash in the
204 // JSON body, with the extension chosen from the content type.
205 func TestHandleUpload(t *testing.T) {
206 s, dir := blTmp(t)
207 defer os.RemoveAll(dir)
208
209 status, _, _ := s.HandleRawWithUpstream("PUT", "/upload", nil, nil, "")
210 if status != 400 {
211 t.Fatalf("empty upload status = %d, want 400", status)
212 }
213
214 body := []byte("hello")
215 headers := map[string]string{"content-type": "text/plain"}
216 status2, h2, resp := s.HandleRawWithUpstream("PUT", "/upload", headers, body, "")
217 if status2 != 200 {
218 t.Fatalf("upload status = %d, want 200", status2)
219 }
220 if h2["Content-Type"] != "application/json" {
221 t.Fatalf("upload response type = %q", h2["Content-Type"])
222 }
223 wantHash := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
224 want := "{\"url\":\"/blossom/" | wantHash | "\",\"sha256\":\"" | wantHash |
225 "\",\"size\":5,\"type\":\"text/plain\"}"
226 if string(resp) != want {
227 t.Fatalf("upload body = %s, want %s", string(resp), want)
228 }
229 if _, err := os.Stat(s.dir|"/"|wantHash); err != nil {
230 t.Fatal("uploaded body was not written to disk")
231 }
232
233 // A known image type adds the matching extension to the returned URL.
234 status3, _, resp3 := s.HandleRawWithUpstream("PUT", "/upload",
235 map[string]string{"content-type": "image/png"}, []byte("png bytes"), "")
236 if status3 != 200 {
237 t.Fatalf("png upload status = %d", status3)
238 }
239 if !blHas(resp3, []byte(".png\"")) {
240 t.Fatalf("png upload missing .png extension: %s", string(resp3))
241 }
242 if !blHas(resp3, []byte("\"url\":\"/blossom/")) {
243 t.Fatalf("png upload missing url: %s", string(resp3))
244 }
245 }
246
247 // blHas is a local substring scan; bytes.Contains routes long haystacks through
248 // bytes.Index's Rabin-Karp fallback, which misses a present needle (reported
249 // separately).
250 func blHas(hay, needle []byte) (ok bool) {
251 for i := 0; i+len(needle) <= len(hay); i++ {
252 if string(hay[i:i+len(needle)]) == string(needle) {
253 return true
254 }
255 }
256 return false
257 }
258