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, 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, "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, "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, "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, "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, "test-token", ) _, err := client.Convert( context.Background(), "6037997512345678", ) if !errors.Is(err, ErrInvalidResponse) { t.Fatalf("error = %v, want ErrInvalidResponse", err) } }