Files
zot/pkg/api/routes_internal_test.go
Akash Kumar 9e13be8b61 fix(api): support multipart range blob pulls (#3995)
* fix(api): support multipart range blob pulls

Signed-off-by: Akash Kumar <meakash7902@gmail.com>

* fix(api): tighten multipart range response

- Drop the redundant deferred closeRangeReaders; the deferred cleanup
  registered when the slice is created already covers all paths.
- Stop copying the request Accept header into each multipart part's
  Content-Type. Accept can be a list of media ranges (e.g.
  "application/octet-stream,*/*"), which is not a valid Content-Type and
  may confuse multipart parsers. RFC 9110 lets us omit it entirely.
- Set Docker-Content-Digest on the partial-content response so range
  pulls expose the same header as a full GET.
- Drop the over-broad build tag on routes_internal_test.go; the parser
  unit test does not need any extension build tags.

Signed-off-by: Akash Kumar <meakash7902@gmail.com>

---------

Signed-off-by: Akash Kumar <meakash7902@gmail.com>
2026-04-27 08:17:08 +03:00

105 lines
2.5 KiB
Go

package api
import (
"reflect"
"strings"
"testing"
)
func TestParseRangeHeader(t *testing.T) {
t.Parallel()
tests := []struct {
name string
header string
size int64
want []httpRange
wantErr bool
}{
{
name: "open ended range",
header: "bytes=0-",
size: 10,
want: []httpRange{{start: 0, end: 9}},
},
{
name: "range end is capped to size",
header: "bytes=0-100",
size: 10,
want: []httpRange{{start: 0, end: 9}},
},
{
name: "suffix range",
header: "bytes=-3",
size: 10,
want: []httpRange{{start: 7, end: 9}},
},
{
name: "oversized suffix range returns whole blob",
header: "bytes=-100",
size: 10,
want: []httpRange{{start: 0, end: 9}},
},
{
name: "ranges are sorted",
header: "bytes=7-8, 0-1",
size: 10,
want: []httpRange{
{start: 0, end: 1},
{start: 7, end: 8},
},
},
{
name: "overlapping and adjacent ranges are coalesced",
header: "bytes=0-2,3-4,6-8,7-9",
size: 10,
want: []httpRange{
{start: 0, end: 4},
{start: 6, end: 9},
},
},
{name: "zero size", header: "bytes=0-", wantErr: true},
{name: "wrong unit", header: "byte=0-1", size: 10, wantErr: true},
{name: "empty range set", header: "bytes=", size: 10, wantErr: true},
{name: "empty range spec", header: "bytes=0-1,", size: 10, wantErr: true},
{name: "zero suffix", header: "bytes=-0", size: 10, wantErr: true},
{name: "bad suffix", header: "bytes=-x", size: 10, wantErr: true},
{name: "bad start", header: "bytes=x-1", size: 10, wantErr: true},
{name: "bad end", header: "bytes=1-x", size: 10, wantErr: true},
{name: "inverted range", header: "bytes=2-1", size: 10, wantErr: true},
{name: "range starts at size", header: "bytes=10-", size: 10, wantErr: true},
{name: "range without dash", header: "bytes=0", size: 10, wantErr: true},
{
name: "too many ranges",
header: "bytes=" + strings.TrimSuffix(strings.Repeat("0-0,", maxRangeSpecCount+1), ","),
size: 10,
wantErr: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
got, err := parseRangeHeader(test.header, test.size)
if test.wantErr {
if err == nil {
t.Fatal("expected parse error")
}
return
}
if err != nil {
t.Fatalf("unexpected parse error: %v", err)
}
if !reflect.DeepEqual(got, test.want) {
t.Fatalf("expected ranges %v, got %v", test.want, got)
}
})
}
}