120 lines
4.3 KiB
Go
120 lines
4.3 KiB
Go
package provider
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewUpstreamStatusErrorRetryHint(t *testing.T) {
|
|
reset := time.Now().Add(15 * time.Minute).Truncate(time.Second)
|
|
cases := []struct {
|
|
name string
|
|
header map[string]string
|
|
want time.Duration // 0 = no hint
|
|
}{
|
|
{"retry-after seconds", map[string]string{"Retry-After": "90"}, 90 * time.Second},
|
|
{"retry-after date", map[string]string{"Retry-After": reset.UTC().Format(http.TimeFormat)}, 15 * time.Minute},
|
|
{"quota exhausted", map[string]string{"X-RateLimit-Remaining": "0", "X-RateLimit-Reset": strconv.FormatInt(reset.Unix(), 10)}, 15 * time.Minute},
|
|
{"quota left is not a hint", map[string]string{"X-RateLimit-Remaining": "12", "X-RateLimit-Reset": strconv.FormatInt(reset.Unix(), 10)}, 0},
|
|
{"no headers", nil, 0},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
resp := &http.Response{StatusCode: http.StatusForbidden, Header: http.Header{}}
|
|
for k, v := range c.header {
|
|
resp.Header.Set(k, v)
|
|
}
|
|
e := NewUpstreamStatusError("u", resp)
|
|
if c.want == 0 {
|
|
if !e.RetryAt.IsZero() {
|
|
t.Fatalf("unexpected hint %v", e.RetryAt)
|
|
}
|
|
return
|
|
}
|
|
if d := time.Until(e.RetryAt) - c.want; d < -2*time.Second || d > 2*time.Second {
|
|
t.Fatalf("retry in %v, want ~%v", time.Until(e.RetryAt), c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSyncResult(t *testing.T) {
|
|
if r := NewSyncResult(`"e"`, nil, time.Hour); r.Failed || r.Etag != `"e"` {
|
|
t.Fatalf("success result = %+v", r)
|
|
}
|
|
|
|
r := NewSyncResult(`"e"`, errors.New("boom"), time.Hour)
|
|
if !r.Failed || r.Backoff != time.Minute || r.MaxBackoff != 10*time.Minute || !r.RetryAt.IsZero() {
|
|
t.Fatalf("plain failure = %+v, want 1m backoff capped at 10m, no hint", r)
|
|
}
|
|
if r := NewSyncResult("", errors.New("boom"), 5*time.Minute); r.MaxBackoff != time.Minute+15*time.Second {
|
|
t.Fatalf("short ttl cap = %v, want ttl/4", r.MaxBackoff)
|
|
}
|
|
|
|
far := &UpstreamStatusError{Status: 403, RetryAt: time.Now().Add(3 * time.Hour)}
|
|
r = NewSyncResult("", fmt.Errorf("scan: %w", far), time.Hour)
|
|
if until := time.Until(r.RetryAt); until > time.Hour || until < 59*time.Minute {
|
|
t.Fatalf("wrapped hint not clamped to ttl: retry in %v", until)
|
|
}
|
|
}
|
|
|
|
func TestAssetStatusErrorClassification(t *testing.T) {
|
|
for status, permanent := range map[int]bool{
|
|
http.StatusNotFound: true, http.StatusGone: true, http.StatusRequestedRangeNotSatisfiable: true,
|
|
http.StatusForbidden: false, http.StatusTooManyRequests: false, http.StatusInternalServerError: false, http.StatusBadGateway: false,
|
|
} {
|
|
err := AssetStatusError("u", &http.Response{StatusCode: status, Header: http.Header{}})
|
|
if errors.Is(err, ErrInvalidPackage) != permanent {
|
|
t.Errorf("status %d: permanent=%v, want %v", status, !permanent, permanent)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadPrefix(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
status int
|
|
cl int64
|
|
crange string
|
|
body string
|
|
n int64
|
|
wantFull bool
|
|
wantErr bool
|
|
}{
|
|
{name: "200 whole object", status: 200, cl: 5, body: "hello", n: 32, wantFull: true},
|
|
{name: "200 larger than n", status: 200, cl: 100, body: "hello", n: 4},
|
|
{name: "200 unknown length short", status: 200, cl: -1, body: "hello", n: 32, wantErr: true},
|
|
{name: "206 whole object", status: 206, crange: "bytes 0-4/5", body: "hello", n: 32, wantFull: true},
|
|
{name: "206 prefix", status: 206, crange: "bytes 0-3/100", body: "hell", n: 4},
|
|
{name: "206 short read", status: 206, crange: "bytes 0-31/100", body: "hello", n: 32, wantErr: true},
|
|
{name: "206 no content-range short", status: 206, body: "hello", n: 32, wantErr: true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resp := &http.Response{StatusCode: tc.status, ContentLength: tc.cl, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(tc.body))}
|
|
if tc.crange != "" {
|
|
resp.Header.Set("Content-Range", tc.crange)
|
|
}
|
|
body, full, err := ReadPrefix(resp, tc.n)
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("err=%v, wantErr=%v", err, tc.wantErr)
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, ErrInvalidPackage) {
|
|
t.Fatalf("short read must be transient: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if full != tc.wantFull || int64(len(body)) > tc.n {
|
|
t.Fatalf("full=%v len=%d, want full=%v", full, len(body), tc.wantFull)
|
|
}
|
|
})
|
|
}
|
|
}
|