767 lines
17 KiB
Go
767 lines
17 KiB
Go
package zarinhub
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestClientConvertSuccess(t *testing.T) {
|
|
const cardNumber = "6037997512345678"
|
|
const expectedIBAN = "IR820540102680020817909002"
|
|
const token = "test-token"
|
|
|
|
server := httptest.NewServer(
|
|
http.HandlerFunc(func(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) {
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf(
|
|
"method = %s, want POST",
|
|
r.Method,
|
|
)
|
|
}
|
|
|
|
if got := r.Header.Get("Authorization"); got != "Bearer "+token {
|
|
t.Errorf(
|
|
"Authorization = %q, want %q",
|
|
got,
|
|
"Bearer "+token,
|
|
)
|
|
}
|
|
|
|
if got := r.Header.Get("Content-Type"); got != "application/json" {
|
|
t.Errorf(
|
|
"Content-Type = %q, want application/json",
|
|
got,
|
|
)
|
|
}
|
|
|
|
var req convertRequest
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Fatalf(
|
|
"decode request: %v",
|
|
err,
|
|
)
|
|
}
|
|
|
|
if req.Card != cardNumber {
|
|
t.Errorf(
|
|
"card = %q, want %q",
|
|
req.Card,
|
|
cardNumber,
|
|
)
|
|
}
|
|
|
|
w.Header().Set(
|
|
"Content-Type",
|
|
"application/json",
|
|
)
|
|
|
|
fmt.Fprint(w, `{
|
|
"data": {
|
|
"bankName": "bankName_value",
|
|
"card": "6037997512345678",
|
|
"deposit": "0101234567001",
|
|
"depositDescription": "depositDescription_value",
|
|
"depositOwners": "depositOwners_value",
|
|
"depositStatus": "depositStatus_value",
|
|
"englishBankName": "englishBankName_value",
|
|
"iban": "IR820540102680020817909002"
|
|
},
|
|
"meta": {
|
|
"code": 0,
|
|
"errorMessage": null,
|
|
"errorType": null,
|
|
"errors": [],
|
|
"isSuccess": true,
|
|
"message": "success",
|
|
"status": "success",
|
|
"trackId": "test-track-id"
|
|
}
|
|
}`)
|
|
}),
|
|
)
|
|
|
|
defer server.Close()
|
|
|
|
client := NewClient(
|
|
&http.Client{
|
|
Timeout: time.Second,
|
|
},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{token}},
|
|
0,
|
|
)
|
|
|
|
result, err := client.Convert(
|
|
context.Background(),
|
|
cardNumber,
|
|
)
|
|
|
|
if err != nil {
|
|
t.Fatalf(
|
|
"Convert() unexpected error: %v",
|
|
err,
|
|
)
|
|
}
|
|
|
|
if result.IBAN != expectedIBAN {
|
|
t.Errorf(
|
|
"IBAN = %q, want %q",
|
|
result.IBAN,
|
|
expectedIBAN,
|
|
)
|
|
}
|
|
|
|
if result.UpstreamHTTPStatus != http.StatusOK {
|
|
t.Errorf(
|
|
"status = %d, want %d",
|
|
result.UpstreamHTTPStatus,
|
|
http.StatusOK,
|
|
)
|
|
}
|
|
|
|
if len(result.RawResponse) == 0 {
|
|
t.Error(
|
|
"expected raw response to be preserved",
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestClientReturnsRejectedForApplicationFailure(
|
|
t *testing.T,
|
|
) {
|
|
server := httptest.NewServer(
|
|
http.HandlerFunc(func(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) {
|
|
w.Header().Set(
|
|
"Content-Type",
|
|
"application/json",
|
|
)
|
|
|
|
fmt.Fprint(w, `{
|
|
"data": {},
|
|
"meta": {
|
|
"trackId": null,
|
|
"status": "error",
|
|
"isSuccess": false,
|
|
"code": -1,
|
|
"message": "request failed",
|
|
"errorMessage": "invalid parameters",
|
|
"errorType": "validation_error",
|
|
"errors": []
|
|
}
|
|
}`)
|
|
}),
|
|
)
|
|
|
|
defer server.Close()
|
|
|
|
client := NewClient(
|
|
&http.Client{
|
|
Timeout: time.Second,
|
|
},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
0,
|
|
)
|
|
|
|
result, err := client.Convert(
|
|
context.Background(),
|
|
"6037997512345678",
|
|
)
|
|
|
|
if !errors.Is(err, ErrRejected) {
|
|
t.Fatalf(
|
|
"error = %v, want ErrRejected",
|
|
err,
|
|
)
|
|
}
|
|
|
|
if result.UpstreamHTTPStatus != http.StatusOK {
|
|
t.Errorf(
|
|
"status = %d, want 200",
|
|
result.UpstreamHTTPStatus,
|
|
)
|
|
}
|
|
|
|
if len(result.RawResponse) == 0 {
|
|
t.Error(
|
|
"expected raw response to be preserved",
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestClientMapsHTTPStatusErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status int
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "unauthorized",
|
|
status: http.StatusUnauthorized,
|
|
wantErr: ErrAuthentication,
|
|
},
|
|
{
|
|
name: "forbidden",
|
|
status: http.StatusForbidden,
|
|
wantErr: ErrAuthentication,
|
|
},
|
|
{
|
|
name: "request timeout",
|
|
status: http.StatusRequestTimeout,
|
|
wantErr: ErrTimeout,
|
|
},
|
|
{
|
|
name: "rate limited",
|
|
status: http.StatusTooManyRequests,
|
|
wantErr: ErrUnavailable,
|
|
},
|
|
{
|
|
name: "server error",
|
|
status: http.StatusInternalServerError,
|
|
wantErr: ErrUnavailable,
|
|
},
|
|
{
|
|
name: "bad request",
|
|
status: http.StatusBadRequest,
|
|
wantErr: ErrRejected,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name,
|
|
func(t *testing.T) {
|
|
server := httptest.NewServer(
|
|
http.HandlerFunc(func(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) {
|
|
w.WriteHeader(tt.status)
|
|
|
|
fmt.Fprint(
|
|
w,
|
|
`{"error":"test"}`,
|
|
)
|
|
}),
|
|
)
|
|
|
|
defer server.Close()
|
|
|
|
client := NewClient(
|
|
&http.Client{
|
|
Timeout: time.Second,
|
|
},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
0,
|
|
)
|
|
|
|
result, err := client.Convert(
|
|
context.Background(),
|
|
"6037997512345678",
|
|
)
|
|
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Fatalf(
|
|
"error = %v, want %v",
|
|
err,
|
|
tt.wantErr,
|
|
)
|
|
}
|
|
|
|
if result.UpstreamHTTPStatus != tt.status {
|
|
t.Errorf(
|
|
"status = %d, want %d",
|
|
result.UpstreamHTTPStatus,
|
|
tt.status,
|
|
)
|
|
}
|
|
|
|
if len(result.RawResponse) == 0 {
|
|
t.Error(
|
|
"expected raw response to be preserved",
|
|
)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestClientReturnsInvalidResponseForMalformedJSON(
|
|
t *testing.T,
|
|
) {
|
|
server := httptest.NewServer(
|
|
http.HandlerFunc(func(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) {
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
fmt.Fprint(
|
|
w,
|
|
`{not-json}`,
|
|
)
|
|
}),
|
|
)
|
|
|
|
defer server.Close()
|
|
|
|
client := NewClient(
|
|
&http.Client{
|
|
Timeout: time.Second,
|
|
},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
0,
|
|
)
|
|
|
|
result, err := client.Convert(
|
|
context.Background(),
|
|
"6037997512345678",
|
|
)
|
|
|
|
if !errors.Is(err, ErrInvalidResponse) {
|
|
t.Fatalf(
|
|
"error = %v, want ErrInvalidResponse",
|
|
err,
|
|
)
|
|
}
|
|
|
|
if result.UpstreamHTTPStatus != http.StatusOK {
|
|
t.Errorf(
|
|
"status = %d, want 200",
|
|
result.UpstreamHTTPStatus,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestClientReturnsTimeout(t *testing.T) {
|
|
server := httptest.NewServer(
|
|
http.HandlerFunc(func(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
) {
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
fmt.Fprint(
|
|
w,
|
|
`{}`,
|
|
)
|
|
}),
|
|
)
|
|
|
|
defer server.Close()
|
|
|
|
client := NewClient(
|
|
&http.Client{
|
|
Timeout: 50 * time.Millisecond,
|
|
},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
0,
|
|
)
|
|
|
|
_, err := client.Convert(
|
|
context.Background(),
|
|
"6037997512345678",
|
|
)
|
|
|
|
if !errors.Is(err, ErrTimeout) {
|
|
t.Fatalf(
|
|
"error = %v, want ErrTimeout",
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestClientRejectsOversizedResponse(t *testing.T) {
|
|
server := httptest.NewServer(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
body := make([]byte, maxResponseBodySize+1)
|
|
for index := range body {
|
|
body[index] = 'x'
|
|
}
|
|
|
|
if _, err := w.Write(body); err != nil {
|
|
t.Errorf("write response: %v", err)
|
|
}
|
|
}),
|
|
)
|
|
defer server.Close()
|
|
|
|
client := NewClient(
|
|
&http.Client{Timeout: time.Second},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
0,
|
|
)
|
|
|
|
_, err := client.Convert(
|
|
context.Background(),
|
|
"6037997512345678",
|
|
)
|
|
|
|
if !errors.Is(err, ErrInvalidResponse) {
|
|
t.Fatalf("error = %v, want ErrInvalidResponse", err)
|
|
}
|
|
}
|
|
|
|
func TestClientRefreshesAndRetriesUnauthorizedRequest(t *testing.T) {
|
|
const expectedIBAN = "IR820540102680020817909002"
|
|
requestCount := 0
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
authorization := r.Header.Get("Authorization")
|
|
|
|
if requestCount == 1 {
|
|
if authorization != "Bearer expired-token" {
|
|
t.Errorf("first authorization = %q", authorization)
|
|
}
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if authorization != "Bearer fresh-token" {
|
|
t.Errorf("second authorization = %q", authorization)
|
|
}
|
|
fmt.Fprintf(w, `{
|
|
"data":{"iban":%q},
|
|
"meta":{"isSuccess":true,"errors":[]}
|
|
}`, expectedIBAN)
|
|
}))
|
|
defer server.Close()
|
|
|
|
tokens := &fakeTokenProvider{tokens: []string{"expired-token", "fresh-token"}}
|
|
client := NewClient(server.Client(), server.URL, tokens, 0)
|
|
|
|
result, err := client.Convert(context.Background(), "6037997512345678")
|
|
if err != nil {
|
|
t.Fatalf("Convert() error = %v", err)
|
|
}
|
|
if result.IBAN != expectedIBAN {
|
|
t.Errorf("IBAN = %q, want %q", result.IBAN, expectedIBAN)
|
|
}
|
|
if requestCount != 2 {
|
|
t.Errorf("request count = %d, want 2", requestCount)
|
|
}
|
|
if len(tokens.invalidated) != 1 || tokens.invalidated[0] != "expired-token" {
|
|
t.Errorf("invalidated tokens = %v", tokens.invalidated)
|
|
}
|
|
}
|
|
|
|
type fakeTokenProvider struct {
|
|
tokens []string
|
|
calls int
|
|
invalidated []string
|
|
}
|
|
|
|
func (p *fakeTokenProvider) AccessToken(context.Context) (string, error) {
|
|
index := p.calls
|
|
if index >= len(p.tokens) {
|
|
index = len(p.tokens) - 1
|
|
}
|
|
p.calls++
|
|
|
|
return p.tokens[index], nil
|
|
}
|
|
|
|
func (p *fakeTokenProvider) Invalidate(token string) {
|
|
p.invalidated = append(p.invalidated, token)
|
|
}
|
|
|
|
func TestRetryDelay(t *testing.T) {
|
|
tests := []struct {
|
|
attempt int
|
|
want time.Duration
|
|
}{
|
|
{attempt: -1, want: 200 * time.Millisecond},
|
|
{attempt: 0, want: 200 * time.Millisecond},
|
|
{attempt: 1, want: 400 * time.Millisecond},
|
|
{attempt: 2, want: 800 * time.Millisecond},
|
|
{attempt: 3, want: 1600 * time.Millisecond},
|
|
{attempt: 4, want: 2 * time.Second},
|
|
{attempt: 10, want: 2 * time.Second},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := retryDelay(tt.attempt)
|
|
if got != tt.want {
|
|
t.Errorf("retryDelay(%d) = %v, want %v", tt.attempt, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewClientSanitizesNegativeRetries(t *testing.T) {
|
|
client := NewClient(nil, "", nil, -1)
|
|
if client.maxRetries != 0 {
|
|
t.Errorf("maxRetries = %d, want 0", client.maxRetries)
|
|
}
|
|
}
|
|
|
|
func TestClientRetriesOnTransientErrorsUntilSuccess(t *testing.T) {
|
|
const expectedIBAN = "IR820540102680020817909002"
|
|
requestCount := 0
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
if requestCount == 1 {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
fmt.Fprint(w, `{"error":"temporary outage"}`)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprintf(w, `{
|
|
"data":{"iban":%q},
|
|
"meta":{"isSuccess":true,"errors":[]}
|
|
}`, expectedIBAN)
|
|
}))
|
|
defer server.Close()
|
|
|
|
var recordedDelays []time.Duration
|
|
client := NewClient(
|
|
&http.Client{Timeout: time.Second},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
2,
|
|
)
|
|
client.retryWait = func(ctx context.Context, delay time.Duration) bool {
|
|
recordedDelays = append(recordedDelays, delay)
|
|
return true
|
|
}
|
|
|
|
result, err := client.Convert(context.Background(), "6037997512345678")
|
|
if err != nil {
|
|
t.Fatalf("Convert() unexpected error = %v", err)
|
|
}
|
|
if result.IBAN != expectedIBAN {
|
|
t.Errorf("IBAN = %q, want %q", result.IBAN, expectedIBAN)
|
|
}
|
|
if requestCount != 2 {
|
|
t.Errorf("request count = %d, want 2", requestCount)
|
|
}
|
|
if len(recordedDelays) != 1 {
|
|
t.Fatalf("len(recordedDelays) = %d, want 1", len(recordedDelays))
|
|
}
|
|
if recordedDelays[0] != 200*time.Millisecond {
|
|
t.Errorf("delay[0] = %v, want 200ms", recordedDelays[0])
|
|
}
|
|
}
|
|
|
|
func TestClientRetriesExhausted(t *testing.T) {
|
|
requestCount := 0
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
w.WriteHeader(http.StatusGatewayTimeout)
|
|
fmt.Fprint(w, `{"error":"gateway timeout"}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
var recordedDelays []time.Duration
|
|
client := NewClient(
|
|
&http.Client{Timeout: time.Second},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
2,
|
|
)
|
|
client.retryWait = func(ctx context.Context, delay time.Duration) bool {
|
|
recordedDelays = append(recordedDelays, delay)
|
|
return true
|
|
}
|
|
|
|
_, err := client.Convert(context.Background(), "6037997512345678")
|
|
if !errors.Is(err, ErrUnavailable) {
|
|
t.Fatalf("error = %v, want ErrUnavailable", err)
|
|
}
|
|
if requestCount != 3 {
|
|
t.Errorf("request count = %d, want 3", requestCount)
|
|
}
|
|
if len(recordedDelays) != 2 {
|
|
t.Fatalf("len(recordedDelays) = %d, want 2", len(recordedDelays))
|
|
}
|
|
if recordedDelays[0] != 200*time.Millisecond {
|
|
t.Errorf("delay[0] = %v, want 200ms", recordedDelays[0])
|
|
}
|
|
if recordedDelays[1] != 400*time.Millisecond {
|
|
t.Errorf("delay[1] = %v, want 400ms", recordedDelays[1])
|
|
}
|
|
}
|
|
|
|
func TestClientDoesNotRetryNonTransientErrors(t *testing.T) {
|
|
requestCount := 0
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprint(w, `{"error":"invalid card format"}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(
|
|
&http.Client{Timeout: time.Second},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
2,
|
|
)
|
|
client.retryWait = func(ctx context.Context, delay time.Duration) bool {
|
|
t.Error("retryWait should not be called for non-transient errors")
|
|
return true
|
|
}
|
|
|
|
_, err := client.Convert(context.Background(), "6037997512345678")
|
|
if !errors.Is(err, ErrRejected) {
|
|
t.Fatalf("error = %v, want ErrRejected", err)
|
|
}
|
|
if requestCount != 1 {
|
|
t.Errorf("request count = %d, want 1", requestCount)
|
|
}
|
|
}
|
|
|
|
func TestClientStopsRetryOnContextCancellation(t *testing.T) {
|
|
requestCount := 0
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
}))
|
|
defer server.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
client := NewClient(
|
|
&http.Client{Timeout: time.Second},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
2,
|
|
)
|
|
client.retryWait = func(c context.Context, delay time.Duration) bool {
|
|
cancel()
|
|
return waitForRetry(ctx, delay)
|
|
}
|
|
|
|
_, err := client.Convert(ctx, "6037997512345678")
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Errorf("error = %v, want context.Canceled", err)
|
|
}
|
|
if requestCount != 1 {
|
|
t.Errorf("request count = %d, want 1", requestCount)
|
|
}
|
|
}
|
|
|
|
func TestClientRetryLogging(t *testing.T) {
|
|
const expectedIBAN = "IR820540102680020817909002"
|
|
requestCount := 0
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
if requestCount == 1 {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
fmt.Fprint(w, `{"error":"temporary outage"}`)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprintf(w, `{
|
|
"data":{"iban":%q},
|
|
"meta":{"isSuccess":true,"errors":[]}
|
|
}`, expectedIBAN)
|
|
}))
|
|
defer server.Close()
|
|
|
|
var logBuf bytes.Buffer
|
|
logger := slog.New(slog.NewJSONHandler(&logBuf, nil))
|
|
|
|
client := NewClient(
|
|
&http.Client{Timeout: time.Second},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
2,
|
|
logger,
|
|
)
|
|
client.retryWait = func(ctx context.Context, delay time.Duration) bool {
|
|
return true
|
|
}
|
|
|
|
result, err := client.Convert(context.Background(), "6037997512345678")
|
|
if err != nil {
|
|
t.Fatalf("Convert() unexpected error = %v", err)
|
|
}
|
|
if result.IBAN != expectedIBAN {
|
|
t.Errorf("IBAN = %q, want %q", result.IBAN, expectedIBAN)
|
|
}
|
|
|
|
logs := logBuf.String()
|
|
|
|
if !strings.Contains(logs, "[INITIAL] zarinhub request") {
|
|
t.Errorf("expected logs to contain '[INITIAL] zarinhub request', got:\n%s", logs)
|
|
}
|
|
if !strings.Contains(logs, `"is_retry":false`) {
|
|
t.Errorf("expected logs to contain '\"is_retry\":false', got:\n%s", logs)
|
|
}
|
|
|
|
if !strings.Contains(logs, "[RETRY] zarinhub request attempt 1") {
|
|
t.Errorf("expected logs to contain '[RETRY] zarinhub request attempt 1', got:\n%s", logs)
|
|
}
|
|
if !strings.Contains(logs, "[RETRY] zarinhub request failed, retrying in") {
|
|
t.Errorf("expected logs to contain '[RETRY] zarinhub request failed, retrying in', got:\n%s", logs)
|
|
}
|
|
if !strings.Contains(logs, `"is_retry":true`) {
|
|
t.Errorf("expected logs to contain '\"is_retry\":true', got:\n%s", logs)
|
|
}
|
|
}
|
|
|
|
func TestClientRetryLoggingExhausted(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
fmt.Fprint(w, `{"error":"down"}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
var logBuf bytes.Buffer
|
|
logger := slog.New(slog.NewJSONHandler(&logBuf, nil))
|
|
|
|
client := NewClient(
|
|
&http.Client{Timeout: time.Second},
|
|
server.URL,
|
|
&fakeTokenProvider{tokens: []string{"test-token"}},
|
|
1,
|
|
logger,
|
|
)
|
|
client.retryWait = func(ctx context.Context, delay time.Duration) bool {
|
|
return true
|
|
}
|
|
|
|
_, err := client.Convert(context.Background(), "6037997512345678")
|
|
if !errors.Is(err, ErrUnavailable) {
|
|
t.Fatalf("error = %v, want ErrUnavailable", err)
|
|
}
|
|
|
|
logs := logBuf.String()
|
|
if !strings.Contains(logs, "[RETRY] zarinhub request failed: all retries exhausted") {
|
|
t.Errorf("expected logs to contain exhausted retries message, got:\n%s", logs)
|
|
}
|
|
}
|