481 lines
8.9 KiB
Go
481 lines
8.9 KiB
Go
package zarinhub
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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}},
|
|
)
|
|
|
|
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"}},
|
|
)
|
|
|
|
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"}},
|
|
)
|
|
|
|
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"}},
|
|
)
|
|
|
|
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"}},
|
|
)
|
|
|
|
_, 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"}},
|
|
)
|
|
|
|
_, 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)
|
|
|
|
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)
|
|
}
|