github.com/Ingenico-ePayments/connect-sdk-go@v0.0.0-20240318153750-1f8cd329b9c9/Client_Idempotence_test.go (about)

     1  package connectsdk
     2  
     3  import (
     4  	"errors"
     5  	"math/rand"
     6  	"net"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/Ingenico-ePayments/connect-sdk-go/communicator"
    15  	"github.com/Ingenico-ePayments/connect-sdk-go/defaultimpl"
    16  	"github.com/Ingenico-ePayments/connect-sdk-go/domain/definitions"
    17  	"github.com/Ingenico-ePayments/connect-sdk-go/domain/payment"
    18  	sdkErrors "github.com/Ingenico-ePayments/connect-sdk-go/errors"
    19  )
    20  
    21  var idempotenceSuccessJSON = `{
    22  	"creationOutput": {
    23  		"additionalReference": "00000200002014254946",
    24  		"externalReference": "000002000020142549460000100001"
    25  	},
    26  	"payment": {
    27  		"id": "000002000020142549460000100001",
    28  		"paymentOutput": {
    29  			"amountOfMoney": {
    30  				"amount": 2345,
    31  				"currencyCode": "CAD"
    32  			},
    33  			"references": {
    34  				"paymentReference": "0"
    35  			},
    36  			"paymentMethod": "card",
    37  			"cardPaymentMethodSpecificOutput": {
    38  				"paymentProductId": 1,
    39  				"authorisationCode": "OK1131",
    40  				"card": {
    41  					"cardNumber": "************9176",
    42  					"expiryDate": "1220"
    43  				},
    44  				"fraudResults": {
    45  					"fraudServiceResult": "error",
    46  					"avsResult": "X",
    47  					"cvvResult": "M"
    48  				}
    49  			}
    50  		},
    51  		"status": "PENDING_APPROVAL",
    52  		"statusOutput": {
    53  			"isCancellable": true,
    54  			"statusCode": 600,
    55  			"statusCodeChangeDateTime": "20150331120036",
    56  			"isAuthorized": true
    57  		}
    58  	}
    59  }`
    60  
    61  var idempotenceRejectJSON = `{
    62  	"errorId": "2c164323-20d3-4e9e-8578-dc562cd7506c-0000003c",
    63  	"errors": [{
    64  		"code": "21000020",
    65  		"requestId": "2001798",
    66  		"message": "VALUE **************** OF FIELD CREDITCARDNUMBER DID NOT PASS THE LUHNCHECK"
    67  	}],
    68  	"paymentResult": {
    69  		"creationOutput": {
    70  			"additionalReference": "00000200002014254436",
    71  			"externalReference": "000002000020142544360000100001"
    72  		},
    73  		"payment": {
    74  			"id": "000002000020142544360000100001",
    75  			"paymentOutput": {
    76  				"amountOfMoney": {
    77  					"amount": 2345,
    78  					"currencyCode": "CAD"
    79  				},
    80  				"references": {
    81  					"paymentReference": "0"
    82  				},
    83  				"paymentMethod": "card",
    84  				"cardPaymentMethodSpecificOutput": {
    85  					"paymentProductId": 1
    86  				}
    87  			},
    88  			"status": "REJECTED",
    89  			"statusOutput": {
    90  				"errors": [{
    91  					"code": "21000020",
    92  					"requestId": "2001798",
    93  					"message": "VALUE **************** OF FIELD CREDITCARDNUMBER DID NOT PASS THE LUHNCHECK"
    94  				}],
    95  				"isCancellable": false,
    96  				"statusCode": 100,
    97  				"statusCodeChangeDateTime": "20150330173151",
    98  				"isAuthorized": false
    99  			}
   100  		}
   101  	}
   102  }`
   103  
   104  var idempotenceDepulicateFailureJSON = `{
   105  	"errorId": "75b0f13a-04a5-41b3-83b8-b295ddb23439-000013c6",
   106  	"errors": [{
   107  		"code": "1409",
   108  		"message": "DUPLICATE REQUEST IN PROGRESS",
   109  		"httpStatusCode": 409
   110  	}]
   111  }`
   112  
   113  func createClient(socketTimeout, connectTimeout time.Duration, port int) (*Client, error) {
   114  	connection, err := defaultimpl.NewDefaultConnection(socketTimeout, connectTimeout, 30*time.Second, 50*time.Second, 500, nil)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	authenticator, err := defaultimpl.NewDefaultAuthenticator(defaultimpl.V1HMAC, "apiKey", "secret")
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	metaDataProvider, err := communicator.NewMetaDataProviderWithIntegrator("Ingenico")
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	endPoint := &url.URL{
   130  		Scheme: "http",
   131  		Host:   "localhost:" + strconv.Itoa(port),
   132  	}
   133  
   134  	session, err := communicator.NewSession(endPoint, connection, authenticator, metaDataProvider)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	marshaller, _ := defaultimpl.NewDefaultMarshaller()
   140  
   141  	communicator, err := communicator.NewCommunicator(session, marshaller)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	return NewClient(communicator)
   147  }
   148  
   149  func createRequest() payment.CreateRequest {
   150  	body := payment.CreateRequest{}
   151  	order := payment.NewOrder()
   152  
   153  	amountOfMoney := definitions.NewAmountOfMoney()
   154  	amountOfMoney.Amount = newInt64(2345)
   155  	amountOfMoney.CurrencyCode = newString("CAD")
   156  	order.AmountOfMoney = amountOfMoney
   157  
   158  	customer := payment.NewCustomer()
   159  
   160  	billingAddress := definitions.NewAddress()
   161  	billingAddress.CountryCode = newString("CA")
   162  	customer.BillingAddress = billingAddress
   163  
   164  	order.Customer = customer
   165  
   166  	cardPaymentMethodSpecificInput := payment.NewCardPaymentMethodSpecificInput()
   167  	cardPaymentMethodSpecificInput.PaymentProductID = newInt32(1)
   168  
   169  	card := definitions.NewCard()
   170  	card.Cvv = newString("123")
   171  	card.CardNumber = newString("4567350000427977")
   172  	card.ExpiryDate = newString("1220")
   173  	cardPaymentMethodSpecificInput.Card = card
   174  
   175  	body.CardPaymentMethodSpecificInput = cardPaymentMethodSpecificInput
   176  
   177  	return body
   178  }
   179  
   180  func newBool(val bool) *bool {
   181  	return &val
   182  }
   183  func newInt32(val int32) *int32 {
   184  	return &val
   185  }
   186  func newInt64(val int64) *int64 {
   187  	return &val
   188  }
   189  func newString(val string) *string {
   190  	return &val
   191  }
   192  
   193  type stoppableListener struct {
   194  	*net.TCPListener
   195  	stop     chan int
   196  	finished sync.WaitGroup
   197  }
   198  
   199  var errStopped = errors.New("listener stopped")
   200  
   201  func (sl *stoppableListener) Accept() (net.Conn, error) {
   202  	sl.finished.Add(1)
   203  	defer sl.finished.Done()
   204  
   205  	for {
   206  		sl.SetDeadline(time.Now().Add(time.Second))
   207  
   208  		newConn, err := sl.TCPListener.Accept()
   209  
   210  		select {
   211  		case <-sl.stop:
   212  			return nil, errStopped
   213  		default:
   214  		}
   215  
   216  		if err != nil {
   217  			netErr, ok := err.(net.Error)
   218  
   219  			if ok && netErr.Timeout() && netErr.Temporary() {
   220  				continue
   221  			}
   222  		}
   223  
   224  		return newConn, err
   225  	}
   226  }
   227  
   228  func (sl *stoppableListener) Stop() {
   229  	close(sl.stop)
   230  	sl.finished.Wait()
   231  }
   232  
   233  func newStoppableListener(l net.Listener) (*stoppableListener, error) {
   234  	tcpL, ok := l.(*net.TCPListener)
   235  
   236  	if !ok {
   237  		return nil, errors.New("cannot wrap listener")
   238  	}
   239  
   240  	return &stoppableListener{tcpL, make(chan int), sync.WaitGroup{}}, nil
   241  }
   242  
   243  func mockServer(server *http.Server, listener net.Listener) (*stoppableListener, error) {
   244  	ls, err := newStoppableListener(listener)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	go server.Serve(ls)
   250  
   251  	return ls, nil
   252  }
   253  
   254  func createRecordRequest(statusCode int, body string, responseHeaders map[string]string, requestHeaders map[string][]string) func(http.ResponseWriter, *http.Request) {
   255  	return func(rw http.ResponseWriter, r *http.Request) {
   256  		for k, v := range r.Header {
   257  			if k == "X-Gcs-Idempotence-Key" {
   258  				k = "X-GCS-Idempotence-Key"
   259  			}
   260  
   261  			requestHeaders[k] = v
   262  		}
   263  
   264  		for k, v := range responseHeaders {
   265  			rw.Header()[k] = []string{v}
   266  		}
   267  
   268  		rw.WriteHeader(statusCode)
   269  
   270  		rw.Write([]byte(body))
   271  	}
   272  }
   273  
   274  const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
   275  
   276  func randString(n int) string {
   277  	b := make([]byte, n, n)
   278  	for i := range b {
   279  		b[i] = letterBytes[rand.Intn(len(letterBytes))]
   280  	}
   281  	return string(b)
   282  }
   283  
   284  func createTestEnvironment(path string, handleFunc http.HandlerFunc) (net.Listener, *stoppableListener, *Client, error) {
   285  	mux := http.NewServeMux()
   286  	mux.Handle(path, handleFunc)
   287  
   288  	httpServer := &http.Server{
   289  		Handler: mux,
   290  	}
   291  
   292  	randomPort := (1 << 12) + rand.Intn(1<<15)
   293  	listener, err := net.Listen("tcp", ":"+strconv.Itoa(randomPort))
   294  	if err != nil {
   295  		return nil, nil, nil, err
   296  	}
   297  
   298  	sl, err := mockServer(httpServer, listener)
   299  	if err != nil {
   300  		return nil, nil, nil, err
   301  	}
   302  
   303  	client, err := createClient(50*time.Second, 50*time.Second, randomPort)
   304  	if err != nil {
   305  		return nil, nil, nil, err
   306  	}
   307  
   308  	return listener, sl, client, nil
   309  }
   310  
   311  func TestIdempotenceFirstRequest(t *testing.T) {
   312  	logPrefix := "TestIdempotenceFirstRequest"
   313  
   314  	idempotenceKey := randString(32)
   315  
   316  	responseHeaders := map[string]string{
   317  		"Content-Type": "application/json",
   318  	}
   319  	requestHeaders := map[string][]string{}
   320  
   321  	context, _ := NewCallContext(idempotenceKey)
   322  
   323  	listener, sl, client, err := createTestEnvironment(
   324  		"/v1/20000/payments",
   325  		createRecordRequest(http.StatusOK, idempotenceSuccessJSON, responseHeaders, requestHeaders))
   326  	if err != nil {
   327  		t.Fatalf("%v: %v", logPrefix, err)
   328  	}
   329  	defer listener.Close()
   330  	defer sl.Close()
   331  	defer client.Close()
   332  
   333  	request := createRequest()
   334  	response, err := client.Merchant("20000").Payments().Create(request, context)
   335  	if err != nil {
   336  		t.Fatalf("%v: %v", logPrefix, err)
   337  	}
   338  
   339  	if response.Payment == nil {
   340  		t.Fatalf("%v: nil payment", logPrefix)
   341  	}
   342  	if response.Payment.ID == nil {
   343  		t.Fatalf("%v: nil paymentID", logPrefix)
   344  	}
   345  
   346  	if idempotenceKey != requestHeaders["X-GCS-Idempotence-Key"][0] {
   347  		t.Fatalf("%v: idempotenceKey mismatch %v %v", logPrefix,
   348  			idempotenceKey, requestHeaders["X-GCS-Idempotence-Key"][0])
   349  	}
   350  
   351  	if context.IdempotenceRequestTimestamp != nil {
   352  		t.Fatalf("%v: timestamp not nil", logPrefix)
   353  	}
   354  }
   355  
   356  func TestIdempotenceSecondRequest(t *testing.T) {
   357  	logPrefix := "TestIdempotenceSecondRequest"
   358  
   359  	idempotenceKey := randString(32)
   360  	idempotenceTimeStamp := time.Now().Sub(time.Unix(0, 0)).Nanoseconds() / int64(time.Millisecond)
   361  
   362  	responseHeaders := map[string]string{
   363  		"Content-Type":                        "application/json",
   364  		"X-GCS-Idempotence-Request-Timestamp": strconv.FormatInt(idempotenceTimeStamp, 10),
   365  	}
   366  	requestHeaders := map[string][]string{}
   367  
   368  	context, _ := NewCallContext(idempotenceKey)
   369  	context.IdempotenceKey = idempotenceKey
   370  
   371  	listener, sl, client, err := createTestEnvironment(
   372  		"/v1/20000/payments",
   373  		createRecordRequest(http.StatusOK, idempotenceSuccessJSON, responseHeaders, requestHeaders))
   374  	if err != nil {
   375  		t.Fatalf("%v: %v", logPrefix, err)
   376  	}
   377  	defer listener.Close()
   378  	defer sl.Close()
   379  	defer client.Close()
   380  
   381  	request := createRequest()
   382  	response, err := client.Merchant("20000").Payments().Create(request, context)
   383  	if err != nil {
   384  		t.Fatalf("%v: %v", logPrefix, err)
   385  	}
   386  
   387  	if response.Payment == nil {
   388  		t.Fatalf("%v: nil payment", logPrefix)
   389  	}
   390  	if response.Payment.ID == nil {
   391  		t.Fatalf("%v: nil paymentID", logPrefix)
   392  	}
   393  
   394  	if idempotenceKey != requestHeaders["X-GCS-Idempotence-Key"][0] {
   395  		t.Fatalf("%v: idempotenceKey mismatch %v %v", logPrefix,
   396  			idempotenceKey, requestHeaders["X-GCS-Idempotence-Key"][0])
   397  	}
   398  
   399  	if context.IdempotenceRequestTimestamp != nil && *context.IdempotenceRequestTimestamp != idempotenceTimeStamp {
   400  		t.Fatalf("%v: timestamp %v", logPrefix, context.IdempotenceRequestTimestamp)
   401  	}
   402  }
   403  
   404  func TestIdempotenceFirstFailure(t *testing.T) {
   405  	logPrefix := "TestIdempotenceFirstFailure"
   406  
   407  	idempotenceKey := randString(32)
   408  	idempotenceTimeStamp := time.Now().Sub(time.Unix(0, 0)).Nanoseconds() / int64(time.Millisecond)
   409  
   410  	responseHeaders := map[string]string{
   411  		"Content-Type":                        "application/json",
   412  		"X-GCS-Idempotence-Request-Timestamp": strconv.FormatInt(idempotenceTimeStamp, 10),
   413  	}
   414  	requestHeaders := map[string][]string{}
   415  
   416  	context, _ := NewCallContext(idempotenceKey)
   417  	context.IdempotenceKey = idempotenceKey
   418  
   419  	listener, sl, client, err := createTestEnvironment(
   420  		"/v1/20000/payments",
   421  		createRecordRequest(http.StatusPaymentRequired, idempotenceRejectJSON, responseHeaders, requestHeaders))
   422  	if err != nil {
   423  		t.Fatalf("%v: %v", logPrefix, err)
   424  	}
   425  	defer listener.Close()
   426  	defer sl.Close()
   427  	defer client.Close()
   428  
   429  	request := createRequest()
   430  	_, err = client.Merchant("20000").Payments().Create(request, context)
   431  	switch ce := err.(type) {
   432  	case *sdkErrors.DeclinedPaymentError:
   433  		{
   434  			if ce.StatusCode() != http.StatusPaymentRequired {
   435  				t.Fatalf("%v: statusCode %v", logPrefix, ce.StatusCode())
   436  			}
   437  			if ce.ResponseBody() != idempotenceRejectJSON {
   438  				t.Fatalf("%v: responseBody %v", logPrefix, ce.ResponseBody())
   439  			}
   440  
   441  			break
   442  		}
   443  	default:
   444  		{
   445  			t.Fatalf("%v: %v", logPrefix, err)
   446  
   447  			break
   448  		}
   449  	}
   450  
   451  	if idempotenceKey != requestHeaders["X-GCS-Idempotence-Key"][0] {
   452  		t.Fatalf("%v: idempotenceKey mismatch %v %v", logPrefix,
   453  			idempotenceKey, requestHeaders["X-GCS-Idempotence-Key"][0])
   454  	}
   455  
   456  	if context.IdempotenceRequestTimestamp != nil && *context.IdempotenceRequestTimestamp != idempotenceTimeStamp {
   457  		t.Fatalf("%v: timestamp %v", logPrefix, context.IdempotenceRequestTimestamp)
   458  	}
   459  }
   460  
   461  func TestIdempotenceSecondFailure(t *testing.T) {
   462  	logPrefix := "TestIdempotenceSecondFailure"
   463  
   464  	idempotenceKey := randString(32)
   465  
   466  	responseHeaders := map[string]string{
   467  		"Content-Type": "application/json",
   468  	}
   469  	requestHeaders := map[string][]string{}
   470  
   471  	context, _ := NewCallContext(idempotenceKey)
   472  	context.IdempotenceKey = idempotenceKey
   473  
   474  	listener, sl, client, err := createTestEnvironment(
   475  		"/v1/20000/payments",
   476  		createRecordRequest(http.StatusPaymentRequired, idempotenceRejectJSON, responseHeaders, requestHeaders))
   477  	if err != nil {
   478  		t.Fatalf("%v: %v", logPrefix, err)
   479  	}
   480  	defer listener.Close()
   481  	defer sl.Close()
   482  	defer client.Close()
   483  
   484  	request := createRequest()
   485  	_, err = client.Merchant("20000").Payments().Create(request, context)
   486  	switch ce := err.(type) {
   487  	case *sdkErrors.DeclinedPaymentError:
   488  		{
   489  			if ce.StatusCode() != http.StatusPaymentRequired {
   490  				t.Fatalf("%v: statusCode %v", logPrefix, ce.StatusCode())
   491  			}
   492  			if ce.ResponseBody() != idempotenceRejectJSON {
   493  				t.Fatalf("%v: responseBody %v", logPrefix, ce.ResponseBody())
   494  			}
   495  
   496  			break
   497  		}
   498  	default:
   499  		{
   500  			t.Fatalf("%v: %v", logPrefix, err)
   501  
   502  			break
   503  		}
   504  	}
   505  
   506  	if idempotenceKey != requestHeaders["X-GCS-Idempotence-Key"][0] {
   507  		t.Fatalf("%v: idempotenceKey mismatch %v %v", logPrefix,
   508  			idempotenceKey, requestHeaders["X-GCS-Idempotence-Key"][0])
   509  	}
   510  
   511  	if context.IdempotenceRequestTimestamp != nil {
   512  		t.Fatalf("%v: timestamp %v", logPrefix, context.IdempotenceRequestTimestamp)
   513  	}
   514  }
   515  
   516  func TestIdempotenceDuplicateRequest(t *testing.T) {
   517  	logPrefix := "TestIdempotenceDuplicateRequest"
   518  
   519  	idempotenceKey := randString(32)
   520  
   521  	responseHeaders := map[string]string{
   522  		"Content-Type": "application/json",
   523  	}
   524  	requestHeaders := map[string][]string{}
   525  
   526  	context, _ := NewCallContext(idempotenceKey)
   527  	context.IdempotenceKey = idempotenceKey
   528  
   529  	listener, sl, client, err := createTestEnvironment(
   530  		"/v1/20000/payments",
   531  		createRecordRequest(http.StatusConflict, idempotenceDepulicateFailureJSON, responseHeaders, requestHeaders))
   532  	if err != nil {
   533  		t.Fatalf("%v: %v", logPrefix, err)
   534  	}
   535  	defer listener.Close()
   536  	defer sl.Close()
   537  	defer client.Close()
   538  
   539  	request := createRequest()
   540  	_, err = client.Merchant("20000").Payments().Create(request, context)
   541  	switch ce := err.(type) {
   542  	case *sdkErrors.IdempotenceError:
   543  		{
   544  			if ce.StatusCode() != http.StatusConflict {
   545  				t.Fatalf("%v: statusCode %v", logPrefix, ce.StatusCode())
   546  			}
   547  			if ce.ResponseBody() != idempotenceDepulicateFailureJSON {
   548  				t.Fatalf("%v: responseBody %v", logPrefix, ce.ResponseBody())
   549  			}
   550  
   551  			break
   552  		}
   553  	default:
   554  		{
   555  			t.Fatalf("%v: %v", logPrefix, err)
   556  
   557  			break
   558  		}
   559  	}
   560  
   561  	if idempotenceKey != requestHeaders["X-GCS-Idempotence-Key"][0] {
   562  		t.Fatalf("%v: idempotenceKey mismatch %v %v", logPrefix,
   563  			idempotenceKey, requestHeaders["X-GCS-Idempotence-Key"][0])
   564  	}
   565  	if idempotenceKey != context.IdempotenceKey {
   566  		t.Fatalf("%v: idempotenceKey mismatch %v %v", logPrefix,
   567  			idempotenceKey, context.IdempotenceKey)
   568  	}
   569  
   570  	if context.IdempotenceRequestTimestamp != nil {
   571  		t.Fatalf("%v: timestamp %v", logPrefix, context.IdempotenceRequestTimestamp)
   572  	}
   573  }