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