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.

Blame · server/raw_range_test.go

Line attribution on main · 101 line(s)

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 }