9c9576b5b9
ci / test (macos-latest) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / lint (push) Has been cancelled
ci / govulncheck (push) Has been cancelled
ci / tidy (push) Has been cancelled
ci / webview (push) Has been cancelled
ci / windows-gui (push) Has been cancelled
release / check (push) Has been cancelled
release / release (push) Has been cancelled
Docs / build (push) Has been cancelled
Docs / deploy-pages (push) Has been cancelled
Docs / deploy-cloudflare (push) Has been cancelled
176 lines
4.8 KiB
Go
176 lines
4.8 KiB
Go
package asset
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestStatusErrorMessage(t *testing.T) {
|
|
cases := map[int]string{
|
|
403: "HTTP 403 Forbidden",
|
|
404: "HTTP 404 Not Found",
|
|
999: "HTTP 999",
|
|
}
|
|
for code, want := range cases {
|
|
if got := (&StatusError{Code: code}).Error(); got != want {
|
|
t.Errorf("StatusError{%d} = %q; want %q", code, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetRetriesTransientThenSucceeds(t *testing.T) {
|
|
var hits int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// 403 on the first try (like bot-protection), then serve the file.
|
|
if atomic.AddInt32(&hits, 1) == 1 {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/css")
|
|
_, _ = w.Write([]byte("body{}"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewDownloader("kage-test", 5*time.Second, 0)
|
|
u, _ := url.Parse(srv.URL + "/style.css")
|
|
res, err := d.Get(context.Background(), u, "")
|
|
if err != nil {
|
|
t.Fatalf("Get after retry: %v", err)
|
|
}
|
|
if !res.IsCSS || string(res.Body) != "body{}" {
|
|
t.Errorf("unexpected result: css=%v body=%q", res.IsCSS, res.Body)
|
|
}
|
|
if hits < 2 {
|
|
t.Errorf("expected a retry; server saw %d hits", hits)
|
|
}
|
|
}
|
|
|
|
func TestGetDoesNotRetryPermanent(t *testing.T) {
|
|
var hits int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&hits, 1)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewDownloader("kage-test", 5*time.Second, 0)
|
|
u, _ := url.Parse(srv.URL + "/missing.png")
|
|
_, err := d.Get(context.Background(), u, "")
|
|
|
|
var se *StatusError
|
|
if !errors.As(err, &se) || se.Code != 404 {
|
|
t.Fatalf("got %v; want StatusError 404", err)
|
|
}
|
|
if hits != 1 {
|
|
t.Errorf("404 should not be retried; server saw %d hits", hits)
|
|
}
|
|
}
|
|
|
|
func TestGetGivesUpAfterRetries(t *testing.T) {
|
|
var hits int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&hits, 1)
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewDownloader("kage-test", 5*time.Second, 0)
|
|
d.Retries = 2
|
|
u, _ := url.Parse(srv.URL + "/rate.css")
|
|
_, err := d.Get(context.Background(), u, "")
|
|
|
|
var se *StatusError
|
|
if !errors.As(err, &se) || se.Code != 429 {
|
|
t.Fatalf("got %v; want StatusError 429", err)
|
|
}
|
|
if hits != 3 { // 1 try + 2 retries
|
|
t.Errorf("expected 3 attempts, server saw %d", hits)
|
|
}
|
|
}
|
|
|
|
func TestGetSkipsOverCapByContentLength(t *testing.T) {
|
|
var hits int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
atomic.AddInt32(&hits, 1)
|
|
body := make([]byte, 4096) // declared via Content-Length
|
|
w.Header().Set("Content-Type", "video/mp4")
|
|
_, _ = w.Write(body)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewDownloader("kage-test", 5*time.Second, 1024) // cap below the body
|
|
u, _ := url.Parse(srv.URL + "/clip.mp4")
|
|
_, err := d.Get(context.Background(), u, "")
|
|
if !errors.Is(err, ErrTooLarge) {
|
|
t.Fatalf("got %v; want ErrTooLarge", err)
|
|
}
|
|
if hits != 1 {
|
|
t.Errorf("an over-cap asset should not be retried; server saw %d hits", hits)
|
|
}
|
|
}
|
|
|
|
func TestGetSkipsOverCapWithoutContentLength(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// A chunked response carries no Content-Length, so the cap is only known
|
|
// after reading past it.
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
fl, _ := w.(http.Flusher)
|
|
chunk := make([]byte, 512)
|
|
for i := 0; i < 8; i++ {
|
|
_, _ = w.Write(chunk)
|
|
if fl != nil {
|
|
fl.Flush()
|
|
}
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewDownloader("kage-test", 5*time.Second, 1024)
|
|
u, _ := url.Parse(srv.URL + "/stream.bin")
|
|
_, err := d.Get(context.Background(), u, "")
|
|
if !errors.Is(err, ErrTooLarge) {
|
|
t.Fatalf("got %v; want ErrTooLarge", err)
|
|
}
|
|
}
|
|
|
|
func TestGetKeepsUnderCap(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write([]byte("small"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
d := NewDownloader("kage-test", 5*time.Second, 1024)
|
|
u, _ := url.Parse(srv.URL + "/logo.png")
|
|
res, err := d.Get(context.Background(), u, "")
|
|
if err != nil {
|
|
t.Fatalf("under-cap asset: %v", err)
|
|
}
|
|
if string(res.Body) != "small" {
|
|
t.Errorf("body = %q; want %q", res.Body, "small")
|
|
}
|
|
}
|
|
|
|
func TestTransientClassification(t *testing.T) {
|
|
transientCodes := []int{403, 408, 425, 429, 500, 502, 503}
|
|
for _, c := range transientCodes {
|
|
if !transient(&StatusError{Code: c}) {
|
|
t.Errorf("status %d should be transient", c)
|
|
}
|
|
}
|
|
for _, c := range []int{400, 401, 404, 410} {
|
|
if transient(&StatusError{Code: c}) {
|
|
t.Errorf("status %d should be permanent", c)
|
|
}
|
|
}
|
|
if transient(context.Canceled) {
|
|
t.Error("context.Canceled should not be transient")
|
|
}
|
|
}
|