arblant
firefox-vpn-client / core / internal / guardian / errors_test.go
errors_test.go76 lines · 2.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
package guardian

import (
	"errors"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"
)

func guardianResponse(status int, header http.Header) *http.Response {
	if header == nil {
		header = make(http.Header)
	}
	return &http.Response{StatusCode: status, Header: header, Body: io.NopCloser(strings.NewReader("body"))}
}

func TestHTTPErrorClassifiesStatus(t *testing.T) {
	tests := []struct {
		status    int
		want      []error
		notWant   []error
		transient bool
	}{
		{http.StatusUnauthorized, []error{ErrTokenInvalid}, []error{ErrNotEntitled, ErrQuotaExceeded}, false},
		{http.StatusForbidden, []error{ErrTokenInvalid, ErrNotEntitled}, []error{ErrQuotaExceeded}, false},
		{http.StatusTooManyRequests, []error{ErrQuotaExceeded}, []error{ErrTokenInvalid}, false},
		{http.StatusUnavailableForLegalReasons, []error{ErrRegionUnavailable}, []error{ErrTokenInvalid, ErrServer}, false},
		{http.StatusServiceUnavailable, []error{ErrServer}, []error{ErrTokenInvalid}, true},
		{http.StatusBadRequest, nil, []error{ErrTokenInvalid, ErrServer, ErrQuotaExceeded}, false},
	}
	for _, tc := range tests {
		err := error(newHTTPError("guardian", guardianResponse(tc.status, nil)))
		for _, want := range tc.want {
			if !errors.Is(err, want) {
				t.Errorf("HTTP %d: expected errors.Is(%v)", tc.status, want)
			}
		}
		for _, notWant := range tc.notWant {
			if errors.Is(err, notWant) {
				t.Errorf("HTTP %d: unexpected errors.Is(%v)", tc.status, notWant)
			}
		}
		if got := IsTransient(err); got != tc.transient {
			t.Errorf("HTTP %d: transient=%v, want %v", tc.status, got, tc.transient)
		}
	}
}

func TestHTTPErrorCarriesRetryAfterAndUsage(t *testing.T) {
	header := make(http.Header)
	header.Set("Retry-After", "120")
	header.Set("X-Quota-Limit", "100")
	header.Set("X-Quota-Remaining", "0")
	header.Set("X-Quota-Reset", "2026-11-01T00:00:00Z")
	err := newHTTPError("guardian", guardianResponse(http.StatusTooManyRequests, header))

	if got := RetryAfter(err); got != 2*time.Minute {
		t.Fatalf("expected Retry-After 2m, got %s", got)
	}
	usage := UsageFromError(err)
	if usage == nil || !usage.Exhausted() || usage.Reset.Format(time.RFC3339) != "2026-11-01T00:00:00Z" {
		t.Fatalf("expected exhausted usage with reset, got %#v", usage)
	}
}

func TestParseRetryAfterAcceptsHTTPDate(t *testing.T) {
	now := time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC)
	if got := parseRetryAfter(now.Add(90*time.Second).Format(http.TimeFormat), now); got != 90*time.Second {
		t.Fatalf("expected 90s, got %s", got)
	}
	if got := parseRetryAfter("soon", now); got != 0 {
		t.Fatalf("expected 0 for invalid value, got %s", got)
	}
}