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.
|
26c9be33
Keenan Nemetz
10d ago
|
1 | package server_test |
| 2 | ||
| 3 | // A raw download is the route a range request is for: resuming a download, | |
| 4 | // previewing a large asset, or seeking in a file. These tests hold the contract: | |
| 5 | // a range gets exactly its bytes with 206, a bad range is refused with 416, and | |
| 6 | // a header the server does not implement still gets the whole file. | |
| 7 | ||
| 8 | import ( | |
| 9 | "io" | |
| 10 | "net/http" | |
| 11 | "strconv" | |
| 12 | "testing" | |
| 13 | ) | |
| 14 | ||
| 15 | func TestRawAnswersByteRanges(t *testing.T) { | |
| 16 | t.Parallel() | |
| 17 | f := newFixture(t) | |
| 18 | f.publish(f.repo) | |
| 19 | const body = "0123456789abcdefghij" // 20 bytes | |
| 20 | work := workRepo(t, map[string]string{"data.txt": body}) | |
| 21 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | |
| 22 | t.Fatalf("push: %s", errs) | |
| 23 | } | |
| 24 | ||
| 25 | get := func(rangeHeader string) (int, string, http.Header) { | |
| 26 | t.Helper() | |
|
ed03b3d1
Keenan Nemetz
6h ago
|
27 | req, err := http.NewRequest(http.MethodGet, f.http.URL+"/"+f.repo+"/raw/main/data.txt", nil) |
|
26c9be33
Keenan Nemetz
10d ago
|
28 | if err != nil { |
| 29 | t.Fatal(err) | |
| 30 | } | |
| 31 | if rangeHeader != "" { | |
| 32 | req.Header.Set("Range", rangeHeader) | |
| 33 | } | |
| 34 | resp, err := http.DefaultClient.Do(req) | |
| 35 | if err != nil { | |
| 36 | t.Fatal(err) | |
| 37 | } | |
| 38 | defer resp.Body.Close() | |
| 39 | got, _ := io.ReadAll(resp.Body) | |
| 40 | return resp.StatusCode, string(got), resp.Header | |
| 41 | } | |
| 42 | ||
| 43 | code, got, h := get("") | |
| 44 | if code != http.StatusOK || got != body { | |
| 45 | t.Fatalf("no range: %d %q, want 200 %q", code, got, body) | |
| 46 | } | |
| 47 | if h.Get("Accept-Ranges") != "bytes" { | |
| 48 | t.Fatalf("Accept-Ranges = %q, want bytes", h.Get("Accept-Ranges")) | |
| 49 | } | |
| 50 | if h.Get("Content-Length") != "20" { | |
| 51 | t.Fatalf("Content-Length = %q, want 20", h.Get("Content-Length")) | |
| 52 | } | |
| 53 | ||
| 54 | cases := []struct { | |
| 55 | name, header, wantBody, wantRange string | |
| 56 | }{ | |
| 57 | {"prefix", "bytes=0-9", "0123456789", "bytes 0-9/20"}, | |
| 58 | {"middle", "bytes=5-9", "56789", "bytes 5-9/20"}, | |
| 59 | {"open ended", "bytes=15-", "fghij", "bytes 15-19/20"}, | |
| 60 | {"end past the file", "bytes=15-99", "fghij", "bytes 15-19/20"}, | |
| 61 | {"suffix", "bytes=-5", "fghij", "bytes 15-19/20"}, | |
| 62 | {"suffix larger than the file", "bytes=-99", body, "bytes 0-19/20"}, | |
| 63 | } | |
| 64 | for _, c := range cases { | |
| 65 | t.Run(c.name, func(t *testing.T) { | |
| 66 | code, got, h := get(c.header) | |
| 67 | if code != http.StatusPartialContent { | |
| 68 | t.Fatalf("%s: status %d, want 206", c.header, code) | |
| 69 | } | |
| 70 | if got != c.wantBody { | |
| 71 | t.Fatalf("%s: body %q, want %q", c.header, got, c.wantBody) | |
| 72 | } | |
| 73 | if h.Get("Content-Range") != c.wantRange { | |
| 74 | t.Fatalf("%s: Content-Range = %q, want %q", c.header, h.Get("Content-Range"), c.wantRange) | |
| 75 | } | |
| 76 | if h.Get("Content-Length") != strconv.Itoa(len(c.wantBody)) { | |
| 77 | t.Fatalf("%s: Content-Length = %q, want %d", c.header, h.Get("Content-Length"), len(c.wantBody)) | |
| 78 | } | |
| 79 | }) | |
| 80 | } | |
| 81 | ||
| 82 | code, got, h = get("bytes=20-") | |
| 83 | if code != http.StatusRequestedRangeNotSatisfiable { | |
| 84 | t.Fatalf("range past the end: status %d, want 416", code) | |
| 85 | } | |
| 86 | if h.Get("Content-Range") != "bytes */20" { | |
| 87 | t.Fatalf("Content-Range = %q, want bytes */20", h.Get("Content-Range")) | |
| 88 | } | |
| 89 | if got != "" { | |
| 90 | t.Fatalf("416 body = %q, want empty", got) | |
| 91 | } | |
| 92 | ||
| 93 | // A header the server does not implement is not an error: RFC 9110 allows | |
| 94 | // answering with the whole representation. | |
| 95 | for _, header := range []string{"bytes=0-1,3-4", "bytes=abc", "bytes=5-3", "items=0-1"} { | |
| 96 | code, got, _ := get(header) | |
| 97 | if code != http.StatusOK || got != body { | |
| 98 | t.Fatalf("%q: %d %q, want 200 and the whole file", header, code, got) | |
| 99 | } | |
| 100 | } | |
| 101 | } |