Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 5s
Tests / Test (Go 1.25) (pull_request) Successful in 25s
Tests / Test (Go stable) (pull_request) Successful in 27s
221 lines
6.1 KiB
Go
221 lines
6.1 KiB
Go
package onlyoffice
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestTransient(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
want bool
|
|
}{
|
|
{nil, false},
|
|
{errors.New("boom"), false},
|
|
{errors.New("GET /api/2.0/files/1.json: 429 Too Many Requests"), true},
|
|
{errors.New("auth: 502 bad gateway"), true},
|
|
{errors.New("POST /x: 503 service unavailable"), true},
|
|
{errors.New("PUT /x: 504 gateway timeout"), true},
|
|
{errors.New("GET /x: 404 not found"), false},
|
|
{errors.New("GET /x: 4290 not a code"), false},
|
|
{&TransientError{Code: 429, Msg: "throttled"}, true},
|
|
{&TransientError{Code: 404, Msg: "missing"}, false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := Transient(c.err); got != c.want {
|
|
t.Errorf("Transient(%v) = %v, want %v", c.err, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoRetryRetriesTransientThenSucceeds(t *testing.T) {
|
|
p := RetryPolicy{Attempts: 5, Base: time.Millisecond, Max: 5 * time.Millisecond}
|
|
attempts := 0
|
|
err := DoRetry(context.Background(), p, func() error {
|
|
attempts++
|
|
if attempts < 3 {
|
|
return errors.New("GET /x: 429 Too Many Requests")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DoRetry: %v", err)
|
|
}
|
|
if attempts != 3 {
|
|
t.Fatalf("attempts = %d, want 3", attempts)
|
|
}
|
|
}
|
|
|
|
func TestDoRetryStopsOnPermanentError(t *testing.T) {
|
|
p := RetryPolicy{Attempts: 5, Base: time.Millisecond, Max: time.Millisecond}
|
|
attempts := 0
|
|
err := DoRetry(context.Background(), p, func() error {
|
|
attempts++
|
|
return errors.New("GET /x: 404 not found")
|
|
})
|
|
if err == nil {
|
|
t.Fatal("want error")
|
|
}
|
|
if attempts != 1 {
|
|
t.Fatalf("attempts = %d, want 1", attempts)
|
|
}
|
|
}
|
|
|
|
func TestDoRetryExhaustsAttemptsOnPersistentTransient(t *testing.T) {
|
|
p := RetryPolicy{Attempts: 3, Base: time.Millisecond, Max: time.Millisecond}
|
|
attempts := 0
|
|
err := DoRetry(context.Background(), p, func() error {
|
|
attempts++
|
|
return errors.New("GET /x: 429 Too Many Requests")
|
|
})
|
|
if !Transient(err) {
|
|
t.Fatalf("want transient error, got %v", err)
|
|
}
|
|
if attempts != 3 {
|
|
t.Fatalf("attempts = %d, want 3", attempts)
|
|
}
|
|
}
|
|
|
|
func TestDoRetryHonoursContextCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
err := DoRetry(ctx, RetryPolicy{Attempts: 3, Base: time.Millisecond, Max: time.Millisecond}, func() error {
|
|
return errors.New("GET /x: 429 Too Many Requests")
|
|
})
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("want context.Canceled, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBackoffDelayExponentialAndCapped(t *testing.T) {
|
|
p := RetryPolicy{Base: 100 * time.Millisecond, Max: 250 * time.Millisecond}
|
|
want := []time.Duration{
|
|
100 * time.Millisecond, // 2^0
|
|
200 * time.Millisecond, // 2^1
|
|
250 * time.Millisecond, // 2^2 capped
|
|
250 * time.Millisecond,
|
|
250 * time.Millisecond,
|
|
}
|
|
for i, w := range want {
|
|
if got := backoffDelay(p, i+1); got != w {
|
|
t.Errorf("backoffDelay(attempt=%d) = %v, want %v", i+1, got, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoRetryWaitsRetryAfterLongerThanBackoff(t *testing.T) {
|
|
const ra = 60 * time.Millisecond
|
|
p := RetryPolicy{Attempts: 2, Base: time.Millisecond, Max: time.Millisecond}
|
|
attempts := 0
|
|
start := time.Now()
|
|
err := DoRetry(context.Background(), p, func() error {
|
|
attempts++
|
|
if attempts == 1 {
|
|
return &TransientError{Code: http.StatusTooManyRequests, RetryAfter: ra, Msg: "GET /x: 429"}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DoRetry: %v", err)
|
|
}
|
|
if elapsed := time.Since(start); elapsed < ra {
|
|
t.Fatalf("waited %v, want >= %v", elapsed, ra)
|
|
}
|
|
}
|
|
|
|
func TestParseRetryAfter(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want time.Duration
|
|
}{
|
|
{"", 0},
|
|
{" 0 ", 0},
|
|
{"-3", 0},
|
|
{"bogus", 0},
|
|
{"120", 120 * time.Second},
|
|
}
|
|
for _, c := range cases {
|
|
if got := parseRetryAfter(c.in); got != c.want {
|
|
t.Errorf("parseRetryAfter(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
future := time.Now().Add(90 * time.Second).UTC().Format(http.TimeFormat)
|
|
if got := parseRetryAfter(future); got < 80*time.Second || got > 95*time.Second {
|
|
t.Errorf("HTTP-date Retry-After = %v, want ~90s", got)
|
|
}
|
|
}
|
|
|
|
// TestGetJSON429CarriesRetryAfter checks that the HTTP layer surfaces a 429 as
|
|
// a typed transient error carrying the parsed Retry-After header.
|
|
func TestGetJSON429CarriesRetryAfter(t *testing.T) {
|
|
t.Setenv("OO_RETRY_ATTEMPTS", "1")
|
|
resetPacingForTest()
|
|
defer resetPacingForTest()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Retry-After", "1")
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
_, _ = w.Write([]byte("busy"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient(Credentials{Url: srv.URL})
|
|
c.token = &Token{Value: "tok", Expires: Time(time.Now().Add(time.Hour))}
|
|
|
|
_, err := c.getJSON(context.Background(), "/api/2.0/project/4.json")
|
|
if err == nil {
|
|
t.Fatal("want error")
|
|
}
|
|
var te *TransientError
|
|
if !errors.As(err, &te) {
|
|
t.Fatalf("want *TransientError, got %T: %v", err, err)
|
|
}
|
|
if te.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("code = %d, want 429", te.Code)
|
|
}
|
|
if te.RetryAfter != time.Second {
|
|
t.Fatalf("RetryAfter = %v, want 1s", te.RetryAfter)
|
|
}
|
|
}
|
|
|
|
// TestGetJSONRetries429ThenSucceeds verifies the end-to-end path: a throttled
|
|
// answer with Retry-After is retried and the eventual success is returned.
|
|
func TestGetJSONRetries429ThenSucceeds(t *testing.T) {
|
|
t.Setenv("OO_RETRY_ATTEMPTS", "3")
|
|
t.Setenv("OO_RETRY_BASE", "10ms")
|
|
t.Setenv("OO_RETRY_MAX", "50ms")
|
|
resetPacingForTest()
|
|
defer resetPacingForTest()
|
|
|
|
var calls int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls++
|
|
if calls == 1 {
|
|
w.Header().Set("Retry-After", "0")
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"response":{}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := NewClient(Credentials{Url: srv.URL})
|
|
c.token = &Token{Value: "tok", Expires: Time(time.Now().Add(time.Hour))}
|
|
|
|
raw, err := c.getJSON(context.Background(), "/api/2.0/project/4.json")
|
|
if err != nil {
|
|
t.Fatalf("getJSON: %v", err)
|
|
}
|
|
if string(raw) != `{"response":{}}` {
|
|
t.Fatalf("raw = %s", raw)
|
|
}
|
|
if calls != 2 {
|
|
t.Fatalf("calls = %d, want 2", calls)
|
|
}
|
|
}
|