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 }