diff --git a/gateways/cybersource/cybersource.go b/gateways/cybersource/cybersource.go index ba86f03d..3f67b668 100644 --- a/gateways/cybersource/cybersource.go +++ b/gateways/cybersource/cybersource.go @@ -85,8 +85,9 @@ func (client *CybersourceClient) AuthorizeWithContext(ctx context.Context, reque Header: responseHeader, } return &response, nil - // Status 401 - during a cybersource outage, most fields were empty and ID was nil - } else if cybersourceResponse.ID == nil { + } + // Status 401 - during a cybersource outage, most fields were empty and ID was nil + if cybersourceResponse.ID == nil { return &sleet.AuthorizationResponse{Success: false}, nil } @@ -116,8 +117,8 @@ func (client *CybersourceClient) AuthorizeWithContext(ctx context.Context, reque response.ExternalTransactionID = cybersourceResponse.ProcessorInformation.TransactionID response.Metadata = buildResponseMetadata(*cybersourceResponse.ProcessorInformation) } - if cybersourceResponse.PaymentInformation != nil { - response.CreatedTokens = buildCreatedTokens(*cybersourceResponse.PaymentInformation) + if cybersourceResponse.TokenInformation != nil { + response.CreatedTokens = buildCreatedTokens(*cybersourceResponse.TokenInformation) } return response, nil } @@ -129,19 +130,19 @@ func buildResponseMetadata(processorInformation ProcessorInformation) map[string return metadata } -func buildCreatedTokens(paymentInformation PaymentInformation) map[sleet.TokenType]string { +func buildCreatedTokens(tokenInformation TokenInformation) map[sleet.TokenType]string { createdTokens := map[sleet.TokenType]string{} - if paymentInformation.Customer != nil { - createdTokens[sleet.TokenTypeCustomer] = paymentInformation.Customer.ID + if tokenInformation.Customer != nil { + createdTokens[sleet.TokenTypeCustomer] = tokenInformation.Customer.ID } - if paymentInformation.PaymentInstrument != nil { - createdTokens[sleet.TokenTypePayment] = paymentInformation.PaymentInstrument.ID + if tokenInformation.PaymentInstrument != nil { + createdTokens[sleet.TokenTypePayment] = tokenInformation.PaymentInstrument.ID } - if paymentInformation.InstrumentIdentifier != nil { - createdTokens[sleet.TokenTypePaymentIdentifier] = paymentInformation.InstrumentIdentifier.ID + if tokenInformation.InstrumentIdentifier != nil { + createdTokens[sleet.TokenTypePaymentIdentifier] = tokenInformation.InstrumentIdentifier.ID } - if paymentInformation.ShippingAddress != nil { - createdTokens[sleet.TokenTypeShippingAddress] = paymentInformation.ShippingAddress.ID + if tokenInformation.ShippingAddress != nil { + createdTokens[sleet.TokenTypeShippingAddress] = tokenInformation.ShippingAddress.ID } if len(createdTokens) == 0 { return nil @@ -282,7 +283,6 @@ func (client *CybersourceClient) sendRequest(ctx context.Context, path string, d } }() - fmt.Printf("status %s\n", resp.Status) // debug respBody, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, nil, err diff --git a/gateways/cybersource/types.go b/gateways/cybersource/types.go index ae35e1be..8b52435a 100644 --- a/gateways/cybersource/types.go +++ b/gateways/cybersource/types.go @@ -39,7 +39,7 @@ type Response struct { ErrorInformation *ErrorInformation `json:"errorInformation,omitempty"` ClientReferenceInformation *ClientReferenceInformation `json:"clientReferenceInformation,omitempty"` ProcessorInformation *ProcessorInformation `json:"processorInformation,omitempty"` - PaymentInformation *PaymentInformation `json:"paymentInformation,omitempty"` + TokenInformation *TokenInformation `json:"tokenInformation,omitempty"` OrderInformation *OrderInformation `json:"orderInformation,omitempty"` ErrorReason *string `json:"reason,omitempty"` ErrorMessage *string `json:"message,omitempty"` @@ -161,8 +161,12 @@ type ShippingDetails struct { // PaymentInformation stores Card or TokenizedCard information (but can be extended to other payment types) type PaymentInformation struct { - Card *CardInformation `json:"card,omitempty"` - TokenizedCard *TokenizedCard `json:"tokenizedCard,omitempty"` + Card *CardInformation `json:"card,omitempty"` + TokenizedCard *TokenizedCard `json:"tokenizedCard,omitempty"` +} + +// TokenInformation stores tokens that were created as a side-effect of a transaction +type TokenInformation struct { Customer *Customer `json:"customer,omitempty"` PaymentInstrument *PaymentInstrument `json:"paymentInstrument,omitempty"` InstrumentIdentifier *InstrumentIdentifier `json:"instrumentIdentifier,omitempty"` @@ -236,12 +240,8 @@ type AuthorizationOptions struct { type ProcessingAction string const ( - ProcessingActionDecisionSkip ProcessingAction = "DECISION_SKIP" - ProcessingActionTokenCreate ProcessingAction = "TOKEN_CREATE" - ProcessingActionConsumerAuthentication ProcessingAction = "CONSUMER_AUTHENTICATION" - ProcessingActionValidateConsumerAuthentication ProcessingAction = "VALIDATE_CONSUMER_AUTHENTICATION" - ProcessingActionAlternatePaymentInitiate ProcessingAction = "AP_INITIATE" - ProcessingActionWatchlistScreening ProcessingAction = "WATCHLIST_SCREENING" + ProcessingActionTokenCreate ProcessingAction = "TOKEN_CREATE" + // There are other actions that we don't use ) // ProcessingActionTokenType defines token types that can be created when using ProcessingActionTokenCreate. diff --git a/integration-tests/cybersource_test.go b/integration-tests/cybersource_test.go index f125b432..ca30137f 100644 --- a/integration-tests/cybersource_test.go +++ b/integration-tests/cybersource_test.go @@ -1,6 +1,7 @@ package test import ( + "net/http" "testing" "github.com/BoltApp/sleet" @@ -11,9 +12,8 @@ import ( func TestAuthorizeAndCaptureAndRefund(t *testing.T) { testCurrency := "USD" - client := cybersource.NewClient(common.Sandbox, getEnv("CYBERSOURCE_ACCOUNT"), getEnv("CYBERSOURCE_API_KEY"), getEnv("CYBERSOURCE_SHARED_SECRET")) + client := getCybersourceClientForTest(t) authRequest := sleet_testing.BaseAuthorizationRequest() - authRequest.ClientTransactionReference = sPtr("[auth]-CUSTOMER-REFERENCE-CODE") // This will be overridden by the level 3 CustomerReference authRequest.BillingAddress = &sleet.Address{ StreetAddress1: sPtr("77 Geary St"), StreetAddress2: sPtr("Floor 4"), @@ -25,7 +25,8 @@ func TestAuthorizeAndCaptureAndRefund(t *testing.T) { Email: sPtr("test@bolt.com"), } authRequest.Level3Data = &sleet.Level3Data{ - CustomerReference: "[auth][l3]-CUSTOMER-REFERENCE-CODE", + // ClientTransactionReference will be overridden by the level 3 CustomerReference + CustomerReference: "l3-" + *authRequest.ClientTransactionReference, TaxAmount: sleet.Amount{Amount: 10, Currency: testCurrency}, DiscountAmount: sleet.Amount{Amount: 0, Currency: testCurrency}, ShippingAmount: sleet.Amount{Amount: 0, Currency: testCurrency}, @@ -62,7 +63,7 @@ func TestAuthorizeAndCaptureAndRefund(t *testing.T) { capResp, err := client.Capture(&sleet.CaptureRequest{ Amount: &authRequest.Amount, TransactionReference: resp.TransactionReference, - ClientTransactionReference: sPtr("[capture]-CUSTOMER-REFERENCE-CODE"), + ClientTransactionReference: sPtr("capture-" + *authRequest.ClientTransactionReference), }) if err != nil { t.Errorf("Expected no error: received: %s", err) @@ -78,7 +79,7 @@ func TestAuthorizeAndCaptureAndRefund(t *testing.T) { refundResp, err := client.Refund(&sleet.RefundRequest{ Amount: &authRequest.Amount, TransactionReference: capResp.TransactionReference, - ClientTransactionReference: sPtr("[refund]-CUSTOMER-REFERENCE-CODE"), + ClientTransactionReference: sPtr("refund-" + *authRequest.ClientTransactionReference), }) if err != nil { t.Errorf("Expected no error: received: %s", err) @@ -92,9 +93,8 @@ func TestAuthorizeAndCaptureWithTokenCreation(t *testing.T) { // Not all CyberSource accounts have this feature. // If this test fails but you are not planning on using tokenization, you can safely ignore the result of this test. t.Skip("Skipping temporarily. TODO winona@bolt.com") - client := cybersource.NewClient(common.Sandbox, getEnv("CYBERSOURCE_ACCOUNT"), getEnv("CYBERSOURCE_API_KEY"), getEnv("CYBERSOURCE_SHARED_SECRET")) + client := getCybersourceClientForTest(t) authRequest := sleet_testing.BaseAuthorizationRequest() - authRequest.ClientTransactionReference = sPtr("[auth]-CUSTOMER-REFERENCE-CODE") authRequest.BillingAddress = &sleet.Address{ StreetAddress1: sPtr("77 Geary St"), StreetAddress2: sPtr("Floor 4"), @@ -145,7 +145,7 @@ func TestAuthorizeAndCaptureWithTokenCreation(t *testing.T) { capResp, err := client.Capture(&sleet.CaptureRequest{ Amount: &authRequest.Amount, TransactionReference: resp.TransactionReference, - ClientTransactionReference: sPtr("[capture]-CUSTOMER-REFERENCE-CODE"), + ClientTransactionReference: sPtr("capture-" + *authRequest.ClientTransactionReference), }) if err != nil { t.Errorf("Expected no error: received: %s", err) @@ -156,9 +156,8 @@ func TestAuthorizeAndCaptureWithTokenCreation(t *testing.T) { } func TestVoid(t *testing.T) { - client := cybersource.NewClient(common.Sandbox, getEnv("CYBERSOURCE_ACCOUNT"), getEnv("CYBERSOURCE_API_KEY"), getEnv("CYBERSOURCE_SHARED_SECRET")) + client := getCybersourceClientForTest(t) authRequest := sleet_testing.BaseAuthorizationRequest() - authRequest.ClientTransactionReference = sPtr("[auth]-CUSTOMER-REFERENCE-CODE") authRequest.BillingAddress = &sleet.Address{ StreetAddress1: sPtr("77 Geary St"), StreetAddress2: sPtr("Floor 4"), @@ -177,10 +176,14 @@ func TestVoid(t *testing.T) { t.Errorf("Expected Success: received: %s", resp.ErrorCode) } + if t.Failed() { + return + } + // void voidResp, err := client.Void(&sleet.VoidRequest{ TransactionReference: resp.TransactionReference, - ClientTransactionReference: sPtr("[void]-CUSTOMER-REFERENCE-CODE"), + ClientTransactionReference: sPtr("void-" + *authRequest.ClientTransactionReference), }) if err != nil { t.Errorf("Expected no error: received: %s", err) @@ -191,7 +194,7 @@ func TestVoid(t *testing.T) { } func TestMissingReference(t *testing.T) { - client := cybersource.NewClient(common.Sandbox, getEnv("CYBERSOURCE_ACCOUNT"), getEnv("CYBERSOURCE_API_KEY"), getEnv("CYBERSOURCE_SHARED_SECRET")) + client := getCybersourceClientForTest(t) request := sleet_testing.BaseRefundRequest() request.TransactionReference = "" resp, err := client.Refund(request) @@ -202,3 +205,19 @@ func TestMissingReference(t *testing.T) { t.Errorf("Expected no response, received %v", resp) } } + +func getCybersourceClientForTest(t *testing.T) *cybersource.CybersourceClient { + helper := sleet_testing.NewTestHelper(t) + + httpClient := &http.Client{ + Transport: helper, + Timeout: common.DefaultTimeout, + } + return cybersource.NewWithHttpClient( + common.Sandbox, + getEnv("CYBERSOURCE_ACCOUNT"), + getEnv("CYBERSOURCE_API_KEY"), + getEnv("CYBERSOURCE_SHARED_SECRET"), + httpClient, + ) +} diff --git a/testing/utils.go b/testing/utils.go index 38b31074..183ca87f 100644 --- a/testing/utils.go +++ b/testing/utils.go @@ -1,9 +1,11 @@ package testing import ( + "bytes" "encoding/json" "encoding/xml" "io/ioutil" + "net/http" "reflect" "testing" @@ -51,3 +53,39 @@ func (h TestHelper) XmlUnmarshal(data []byte, destination interface{}) { h.t.Fatalf("Error unmarshaling json %q \n", err) } } + +// RoundTrip allows TestHelper to act as a HTTP RoundTripper that logs requests and responses. +// This can be used by overriding the HTTP client used by a PSP client to be the TestHelper instance. +// +// Example: +// +// helper := sleet_testing.NewTestHelper(t) +// httpClient := &http.Client{ +// Transport: helper, +// Timeout: common.DefaultTimeout, +// } +func (h TestHelper) RoundTrip(req *http.Request) (*http.Response, error) { + h.t.Helper() + + resp, err := http.DefaultTransport.RoundTrip(req) + + reqBodyStream, _ := req.GetBody() + defer reqBodyStream.Close() + reqBody, _ := ioutil.ReadAll(reqBodyStream) + + respBodyStream := resp.Body + defer respBodyStream.Close() + respBody, _ := ioutil.ReadAll(respBodyStream) + // we need to replace the resp body to be read again by the actual handler + resp.Body = ioutil.NopCloser(bytes.NewBuffer(respBody)) + + h.t.Logf( + "logTransport HTTP request\n"+ + "-> status %s\n"+ + "-v request\n"+ + string(reqBody)+"\n"+ + "-v response\n"+ + string(respBody)+"\n\n", + resp.Status) + return resp, err +}