diff --git a/src/main/java/com/checkout/ApacheHttpClientTransport.java b/src/main/java/com/checkout/ApacheHttpClientTransport.java index a6dd328e..b4dfd9dd 100644 --- a/src/main/java/com/checkout/ApacheHttpClientTransport.java +++ b/src/main/java/com/checkout/ApacheHttpClientTransport.java @@ -83,12 +83,14 @@ class ApacheHttpClientTransport implements Transport { } @Override - public CompletableFuture invoke(final ClientOperation clientOperation, - final String path, - final SdkAuthorization authorization, - final String requestBody, - final String idempotencyKey, - final Map queryParams) { + public CompletableFuture invoke( + final ClientOperation clientOperation, + final String path, + final SdkAuthorization authorization, + final String requestBody, + final String idempotencyKey, + final Map queryParams + ) { return CompletableFuture.supplyAsync(() -> { final HttpUriRequest request; switch (clientOperation) { @@ -129,7 +131,40 @@ public CompletableFuture invoke(final ClientOperation clientOperation, } @Override - public CompletableFuture submitFile(final String path, final SdkAuthorization authorization, final AbstractFileRequest fileRequest) { + public CompletableFuture invoke( + final ClientOperation clientOperation, + final String path, + final SdkAuthorization authorization, + final String requestBody, + final String idempotencyKey, + final Map queryParams, + final String contentType + ) { + return CompletableFuture.supplyAsync(() -> { + final HttpPost request = new HttpPost(getRequestUrl(path)); + + if (idempotencyKey != null) { + request.setHeader("Cko-Idempotency-Key", idempotencyKey); + } + + request.setHeader("User-Agent", PROJECT_NAME + "/" + getVersionFromManifest()); + request.setHeader("Accept", ACCEPT_JSON); + request.setHeader("Authorization", authorization.getAuthorizationHeader()); + + if (requestBody != null) { + request.setEntity(new StringEntity(requestBody, ContentType.parse(contentType))); + } + + return performCall(authorization, null, request, clientOperation); + }, executor); + } + + @Override + public CompletableFuture submitFile( + final String path, + final SdkAuthorization authorization, + final AbstractFileRequest fileRequest + ) { return CompletableFuture.supplyAsync(() -> { final HttpPost request = new HttpPost(getRequestUrl(path)); request.setEntity(getMultipartFileEntity(fileRequest)); diff --git a/src/main/java/com/checkout/ApiClient.java b/src/main/java/com/checkout/ApiClient.java index 85e4c082..1e7c1fb4 100644 --- a/src/main/java/com/checkout/ApiClient.java +++ b/src/main/java/com/checkout/ApiClient.java @@ -1,8 +1,10 @@ package com.checkout; import com.checkout.common.AbstractFileRequest; +import org.apache.http.NameValuePair; import java.lang.reflect.Type; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -24,6 +26,8 @@ public interface ApiClient { CompletableFuture postAsync(String path, SdkAuthorization authorization, Map> resultTypeMappings, Object request, String idempotencyKey); + CompletableFuture postFormUrlEncodedAsync(String path, SdkAuthorization authorization, List formParams, Class responseType); + CompletableFuture deleteAsync(String path, SdkAuthorization authorization); CompletableFuture deleteAsync(String path, SdkAuthorization authorization, Class responseType); diff --git a/src/main/java/com/checkout/ApiClientImpl.java b/src/main/java/com/checkout/ApiClientImpl.java index 36fb15ed..fcfac747 100644 --- a/src/main/java/com/checkout/ApiClientImpl.java +++ b/src/main/java/com/checkout/ApiClientImpl.java @@ -16,14 +16,17 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import com.checkout.common.AbstractFileRequest; import com.checkout.common.CheckoutUtils; import com.google.gson.reflect.TypeToken; +import org.apache.http.NameValuePair; public class ApiClientImpl implements ApiClient { @@ -176,6 +179,39 @@ private CompletableFuture sendRequestAsync(final Cli .thenApply(response -> deserialize(response, responseType)); } + @Override + public CompletableFuture postFormUrlEncodedAsync( + final String path, + final SdkAuthorization authorization, + final List formParams, + final Class responseType + ) { + validateParams(PATH, path, AUTHORIZATION, authorization, "formParams", formParams); + + final String body = formParams.stream() + .map(p -> p.getName() + "=" + encode(p.getValue())) + .collect(Collectors.joining("&")); + + return transport.invoke( + ClientOperation.POST, + path, + authorization, + body, + null, + null, + "application/x-www-form-urlencoded" + ).thenApply(this::errorCheck) + .thenApply(response -> deserialize(response, responseType)); + } + + private String encode(final String value) { + try { + return java.net.URLEncoder.encode(value, "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + throw new CheckoutException("Failed to encode form param", e); + } + } + private Response errorCheck(final Response response) { if (!CheckoutUtils.isSuccessHttpStatusCode(response.getStatusCode())) { final Map errorDetails = serializer.fromJson(response.getBody()); diff --git a/src/main/java/com/checkout/Transport.java b/src/main/java/com/checkout/Transport.java index e2ad26e8..39f76fe3 100644 --- a/src/main/java/com/checkout/Transport.java +++ b/src/main/java/com/checkout/Transport.java @@ -7,8 +7,28 @@ public interface Transport { - CompletableFuture invoke(ClientOperation clientOperation, String path, SdkAuthorization authorization, String jsonRequest, String idempotencyKey, Map queryParams); + CompletableFuture invoke( + ClientOperation clientOperation, + String path, + SdkAuthorization authorization, + String jsonRequest, + String idempotencyKey, + Map queryParams + ); - CompletableFuture submitFile(String path, SdkAuthorization authorization, AbstractFileRequest fileRequest); + CompletableFuture invoke( + ClientOperation clientOperation, + String path, + SdkAuthorization authorization, + String requestBody, + String idempotencyKey, + Map queryParams, + String contentType + ); + CompletableFuture submitFile( + String path, + SdkAuthorization authorization, + AbstractFileRequest fileRequest + ); } diff --git a/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/requests/RequestAnAccessTokenRequest.java b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/requests/RequestAnAccessTokenRequest.java new file mode 100644 index 00000000..8a773bd3 --- /dev/null +++ b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/requests/RequestAnAccessTokenRequest.java @@ -0,0 +1,59 @@ +package com.checkout.cardissuing.cardholderaccesstokens.requests; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class RequestAnAccessTokenRequest { + + @SerializedName("grant_type") + private String grantType; + + /** + * Access key ID + */ + @SerializedName("client_id") + private String clientId; + + /** + * Access key secret + */ + @SerializedName("client_secret") + private String clientSecret; + + /** + * The cardholder's unique identifier + */ + @SerializedName("cardholder_id") + private String cardholderId; + + /** + * Specifies if the request is for a single-use token. Single-use tokens are required for sensitive endpoints + */ + @SerializedName("single_use") + private Boolean singleUse; + + public List toFormParams() { + final List params = new ArrayList<>(); + params.add(new BasicNameValuePair("grant_type", grantType)); + params.add(new BasicNameValuePair("client_id", clientId)); + params.add(new BasicNameValuePair("client_secret", clientSecret)); + params.add(new BasicNameValuePair("cardholder_id", cardholderId)); + if (singleUse != null) { + params.add(new BasicNameValuePair("single_use", singleUse.toString())); + } + return params; + } + +} diff --git a/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/responses/RequestAnAccessTokenResponse.java b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/responses/RequestAnAccessTokenResponse.java new file mode 100644 index 00000000..0a327825 --- /dev/null +++ b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/responses/RequestAnAccessTokenResponse.java @@ -0,0 +1,28 @@ +package com.checkout.cardissuing.cardholderaccesstokens.responses; + +import com.checkout.HttpMetadata; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class RequestAnAccessTokenResponse extends HttpMetadata { + + @SerializedName("access_token") + private String accessToken; + + @SerializedName("token_type") + private String tokenType; + + /** + * The remaining time the access token is valid for, in seconds + */ + @SerializedName("expires_in") + private Double expiresIn; + + private String scope; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/IssuingClient.java b/src/main/java/com/checkout/issuing/IssuingClient.java index d590a986..cf476d0e 100644 --- a/src/main/java/com/checkout/issuing/IssuingClient.java +++ b/src/main/java/com/checkout/issuing/IssuingClient.java @@ -1,6 +1,8 @@ package com.checkout.issuing; import com.checkout.EmptyResponse; +import com.checkout.cardissuing.cardholderaccesstokens.requests.RequestAnAccessTokenRequest; +import com.checkout.cardissuing.cardholderaccesstokens.responses.RequestAnAccessTokenResponse; import com.checkout.common.IdResponse; import com.checkout.issuing.cardholders.CardholderCardsResponse; import com.checkout.issuing.cardholders.CardholderDetailsResponse; @@ -70,6 +72,8 @@ public interface IssuingClient { CompletableFuture removeCardControl(final String controlId); + CompletableFuture RequestAnAccessToken(final RequestAnAccessTokenRequest requestAnAccessTokenRequest); + CompletableFuture simulateAuthorization(final CardAuthorizationRequest cardAuthorizationRequest); CompletableFuture simulateIncrementingAuthorization( diff --git a/src/main/java/com/checkout/issuing/IssuingClientImpl.java b/src/main/java/com/checkout/issuing/IssuingClientImpl.java index e27b303d..f26c3474 100644 --- a/src/main/java/com/checkout/issuing/IssuingClientImpl.java +++ b/src/main/java/com/checkout/issuing/IssuingClientImpl.java @@ -4,7 +4,11 @@ import com.checkout.ApiClient; import com.checkout.CheckoutConfiguration; import com.checkout.EmptyResponse; +import com.checkout.PlatformType; +import com.checkout.SdkAuthorization; import com.checkout.SdkAuthorizationType; +import com.checkout.cardissuing.cardholderaccesstokens.requests.RequestAnAccessTokenRequest; +import com.checkout.cardissuing.cardholderaccesstokens.responses.RequestAnAccessTokenResponse; import com.checkout.common.IdResponse; import com.checkout.issuing.cardholders.CardholderCardsResponse; import com.checkout.issuing.cardholders.CardholderDetailsResponse; @@ -60,6 +64,12 @@ public class IssuingClientImpl extends AbstractClient implements IssuingClient { private static final String CONTROLS_PATH = "controls"; + private static final String ACCESS_PATH = "access"; + + private static final String CONNECT_PATH = "connect"; + + private static final String TOKEN_PATH = "token"; + private static final String SIMULATE_PATH = "simulate"; private static final String AUTHORIZATIONS_PATH = "authorizations"; @@ -278,6 +288,19 @@ public CompletableFuture removeCardControl(final String controlId) { ); } + @Override + public CompletableFuture RequestAnAccessToken ( + final RequestAnAccessTokenRequest requestAnAccessTokenRequest + ) { + validateParams("requestAnAccessTokenRequest", requestAnAccessTokenRequest); + return apiClient.postFormUrlEncodedAsync( + buildPath(ISSUING_PATH, ACCESS_PATH, CONNECT_PATH, TOKEN_PATH), + new SdkAuthorization(PlatformType.DEFAULT, ""), + requestAnAccessTokenRequest.toFormParams(), + RequestAnAccessTokenResponse.class + ); + } + @Override public CompletableFuture simulateAuthorization(final CardAuthorizationRequest cardAuthorizationRequest) { validateParams("cardAuthorizationRequest", cardAuthorizationRequest); diff --git a/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java b/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java new file mode 100644 index 00000000..310604b1 --- /dev/null +++ b/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java @@ -0,0 +1,63 @@ +package com.checkout.cardissuing.cardholderaccesstokens; + +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.cardissuing.cardholderaccesstokens.requests.RequestAnAccessTokenRequest; +import com.checkout.cardissuing.cardholderaccesstokens.responses.RequestAnAccessTokenResponse; +import com.checkout.issuing.IssuingClient; +import com.checkout.issuing.IssuingClientImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class IssuinClientImplRequestAnAccessTokenTest { + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + private IssuingClient client; + + @BeforeEach + void setUp() { + when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH)).thenReturn(authorization); + when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + client = new IssuingClientImpl(apiClient, configuration); + } + + @Test + void shouldRequestAccessToken() throws ExecutionException, InterruptedException { + RequestAnAccessTokenRequest request = mock(RequestAnAccessTokenRequest.class); + RequestAnAccessTokenResponse response = mock(RequestAnAccessTokenResponse.class); + + when(apiClient.postAsync( + "issuing/cardholder-access/token", + authorization, + RequestAnAccessTokenResponse.class, + request, + null + )).thenReturn(CompletableFuture.completedFuture(response)); + + CompletableFuture future = client.RequestAnAccessToken(request); + + assertNotNull(future.get()); + assertEquals(response, future.get()); + } +}