diff --git a/build.gradle b/build.gradle index ea2bdeb0..9aa5cc63 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { // to publish packages id 'maven-publish' // code linting - id "com.diffplug.spotless" version "6.18.0" + id "com.diffplug.spotless" version "6.19.0" // test coverage id 'jacoco' id 'com.github.kt3k.coveralls' version '2.12.2' diff --git a/gradle.properties b/gradle.properties index 3a68e29e..903ac365 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Main properties group=com.truelayer archivesBaseName=truelayer-java -version=7.0.0 +version=8.0.0 # Artifacts properties sonatype_repository_url=https://s01.oss.sonatype.org/service/local/ diff --git a/src/main/java/com/truelayer/java/ITrueLayerClient.java b/src/main/java/com/truelayer/java/ITrueLayerClient.java index 235a4329..dd0d5db5 100644 --- a/src/main/java/com/truelayer/java/ITrueLayerClient.java +++ b/src/main/java/com/truelayer/java/ITrueLayerClient.java @@ -7,9 +7,9 @@ import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.mandates.IMandatesHandler; import com.truelayer.java.merchantaccounts.IMerchantAccountsHandler; -import com.truelayer.java.payments.IPaymentsApi; +import com.truelayer.java.payments.IPaymentsHandler; import com.truelayer.java.paymentsproviders.IPaymentsProvidersHandler; -import com.truelayer.java.payouts.IPayoutsApi; +import com.truelayer.java.payouts.IPayoutsHandler; import java.util.concurrent.CompletableFuture; /** @@ -27,7 +27,7 @@ public interface ITrueLayerClient { * Entrypoint for payments endpoints. * @return a utility to interact with payments endpoints. */ - IPaymentsApi payments(); + IPaymentsHandler payments(); /** * Entrypoint for payments providers endpoints. @@ -51,7 +51,7 @@ public interface ITrueLayerClient { * Entrypoint for payouts endpoints. * @return a utility to interact with payouts endpoints. */ - IPayoutsApi payouts(); + IPayoutsHandler payouts(); /** * Entrypoint for Hosted Payment Page related services. diff --git a/src/main/java/com/truelayer/java/TrueLayerClient.java b/src/main/java/com/truelayer/java/TrueLayerClient.java index d1e6f9ed..e1c45b89 100644 --- a/src/main/java/com/truelayer/java/TrueLayerClient.java +++ b/src/main/java/com/truelayer/java/TrueLayerClient.java @@ -1,16 +1,16 @@ package com.truelayer.java; import com.truelayer.java.auth.IAuthenticationHandler; -import com.truelayer.java.commonapi.ICommonApi; +import com.truelayer.java.commonapi.ICommonHandler; import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.mandates.IMandatesHandler; import com.truelayer.java.merchantaccounts.IMerchantAccountsHandler; -import com.truelayer.java.payments.IPaymentsApi; +import com.truelayer.java.payments.IPaymentsHandler; import com.truelayer.java.paymentsproviders.IPaymentsProvidersHandler; -import com.truelayer.java.payouts.IPayoutsApi; +import com.truelayer.java.payouts.IPayoutsHandler; import java.util.concurrent.CompletableFuture; import lombok.AllArgsConstructor; import org.apache.commons.lang3.ObjectUtils; @@ -23,22 +23,24 @@ */ @AllArgsConstructor public class TrueLayerClient implements ITrueLayerClient { + private IAuthenticationHandler authenticationHandler; - private IPaymentsApi paymentsHandler; + private IPaymentsHandler paymentsHandler; private IPaymentsProvidersHandler paymentsProvidersHandler; private IMerchantAccountsHandler merchantAccountsHandler; private IMandatesHandler mandatesHandler; - private IPayoutsApi payoutsHandler; + private IPayoutsHandler payoutsHandler; + private ICommonHandler commonHandler; + private IHostedPaymentPageLinkBuilder hostedPaymentPageLinkBuilder; - private ICommonApi commonApi; public TrueLayerClient( IAuthenticationHandler authenticationHandler, IHostedPaymentPageLinkBuilder hostedPaymentPageLinkBuilder, - ICommonApi commonApi) { + ICommonHandler commonHandler) { this.authenticationHandler = authenticationHandler; this.hostedPaymentPageLinkBuilder = hostedPaymentPageLinkBuilder; - this.commonApi = commonApi; + this.commonHandler = commonHandler; } /** @@ -61,7 +63,7 @@ public IAuthenticationHandler auth() { * {@inheritDoc} */ @Override - public IPaymentsApi payments() { + public IPaymentsHandler payments() { if (ObjectUtils.isEmpty(paymentsHandler)) { throw buildInitializationException("payments"); } @@ -73,6 +75,9 @@ public IPaymentsApi payments() { */ @Override public IPaymentsProvidersHandler paymentsProviders() { + if (ObjectUtils.isEmpty(paymentsProvidersHandler)) { + throw buildInitializationException("payment providers"); + } return paymentsProvidersHandler; } @@ -96,7 +101,7 @@ public IMandatesHandler mandates() { } @Override - public IPayoutsApi payouts() { + public IPayoutsHandler payouts() { if (ObjectUtils.isEmpty(payoutsHandler)) { throw buildInitializationException("payouts"); } @@ -117,7 +122,7 @@ public IHostedPaymentPageLinkBuilder hpp() { @Override public CompletableFuture> submitPaymentReturnParameters( SubmitPaymentReturnParametersRequest request) { - return commonApi.submitPaymentReturnParameters(request); + return commonHandler.submitPaymentReturnParameters(request); } private TrueLayerException buildInitializationException(String handlerName) { diff --git a/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java b/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java index e9fa0105..fbcc48c4 100644 --- a/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java +++ b/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java @@ -4,7 +4,9 @@ import com.truelayer.java.auth.AuthenticationHandler; import com.truelayer.java.auth.IAuthenticationHandler; +import com.truelayer.java.commonapi.CommonHandler; import com.truelayer.java.commonapi.ICommonApi; +import com.truelayer.java.commonapi.ICommonHandler; import com.truelayer.java.hpp.HostedPaymentPageLinkBuilder; import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder; import com.truelayer.java.http.OkHttpClientFactory; @@ -19,9 +21,13 @@ import com.truelayer.java.merchantaccounts.IMerchantAccountsHandler; import com.truelayer.java.merchantaccounts.MerchantAccountsHandler; import com.truelayer.java.payments.IPaymentsApi; +import com.truelayer.java.payments.IPaymentsHandler; +import com.truelayer.java.payments.PaymentsHandler; import com.truelayer.java.paymentsproviders.IPaymentsProvidersHandler; import com.truelayer.java.paymentsproviders.PaymentsProvidersHandler; import com.truelayer.java.payouts.IPayoutsApi; +import com.truelayer.java.payouts.IPayoutsHandler; +import com.truelayer.java.payouts.PayoutsHandler; import com.truelayer.java.versioninfo.LibraryInfoLoader; import java.time.Clock; import java.time.Duration; @@ -207,20 +213,22 @@ public TrueLayerClient build() { // We're reusing a client with only User agent and Idempotency key interceptors and give it our base payment // endpoint - ICommonApi commonApiHandler = RetrofitFactory.build(authHttpClient, environment.getPaymentsApiUri()) + ICommonApi commonApi = RetrofitFactory.build(authHttpClient, environment.getPaymentsApiUri()) .create(ICommonApi.class); + ICommonHandler commonHandler = new CommonHandler(commonApi); // As per our RFC, if signing options is not configured we create a client which is able to interact // with the Authentication API only if (isEmpty(signingOptions)) { - return new TrueLayerClient(authenticationHandler, hppLinkBuilder, commonApiHandler); + return new TrueLayerClient(authenticationHandler, hppLinkBuilder, commonHandler); } OkHttpClient paymentsHttpClient = httpClientFactory.buildPaymentsApiClient( authHttpClient, authenticationHandler, signingOptions, credentialsCache); - IPaymentsApi paymentsHandler = RetrofitFactory.build(paymentsHttpClient, environment.getPaymentsApiUri()) + IPaymentsApi paymentsApi = RetrofitFactory.build(paymentsHttpClient, environment.getPaymentsApiUri()) .create(IPaymentsApi.class); + IPaymentsHandler paymentsHandler = new PaymentsHandler(paymentsApi); IPaymentsProvidersHandler paymentsProvidersHandler = PaymentsProvidersHandler.New() .clientCredentials(clientCredentials) @@ -236,8 +244,9 @@ public TrueLayerClient build() { .create(IMandatesApi.class); IMandatesHandler mandatesHandler = new MandatesHandler(mandatesApi); - IPayoutsApi payoutsHandler = RetrofitFactory.build(paymentsHttpClient, environment.getPaymentsApiUri()) + IPayoutsApi payoutsApi = RetrofitFactory.build(paymentsHttpClient, environment.getPaymentsApiUri()) .create(IPayoutsApi.class); + IPayoutsHandler payoutsHandler = new PayoutsHandler(payoutsApi); return new TrueLayerClient( authenticationHandler, @@ -246,7 +255,7 @@ public TrueLayerClient build() { merchantAccountsHandler, mandatesHandler, payoutsHandler, - hppLinkBuilder, - commonApiHandler); + commonHandler, + hppLinkBuilder); } } diff --git a/src/main/java/com/truelayer/java/commonapi/CommonHandler.java b/src/main/java/com/truelayer/java/commonapi/CommonHandler.java new file mode 100644 index 00000000..ea47a669 --- /dev/null +++ b/src/main/java/com/truelayer/java/commonapi/CommonHandler.java @@ -0,0 +1,29 @@ +package com.truelayer.java.commonapi; + +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; + +import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; +import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; +import java.util.concurrent.CompletableFuture; +import lombok.Value; + +@Value +public class CommonHandler implements ICommonHandler { + + ICommonApi commonApi; + + @Override + public CompletableFuture> submitPaymentReturnParameters( + SubmitPaymentReturnParametersRequest request) { + return commonApi.submitPaymentReturnParameters(emptyMap(), request); + } + + @Override + public CompletableFuture> submitPaymentReturnParameters( + Headers headers, SubmitPaymentReturnParametersRequest request) { + return commonApi.submitPaymentReturnParameters(toMap(headers), request); + } +} diff --git a/src/main/java/com/truelayer/java/commonapi/ICommonApi.java b/src/main/java/com/truelayer/java/commonapi/ICommonApi.java index 3782b6cf..e07486b3 100644 --- a/src/main/java/com/truelayer/java/commonapi/ICommonApi.java +++ b/src/main/java/com/truelayer/java/commonapi/ICommonApi.java @@ -3,8 +3,10 @@ import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; import com.truelayer.java.http.entities.ApiResponse; +import java.util.Map; import java.util.concurrent.CompletableFuture; import retrofit2.http.Body; +import retrofit2.http.HeaderMap; import retrofit2.http.POST; /** @@ -16,11 +18,12 @@ public interface ICommonApi { /** * Submits payments return parameters. + * @param headers map representing custom HTTP headers to be sent * @param request a submit payment return parameters payload * @return the response of the Submit payment returns parameters operation * @see Submit payments return parameters API reference */ @POST("/payments-provider-return") CompletableFuture> submitPaymentReturnParameters( - @Body SubmitPaymentReturnParametersRequest request); + @HeaderMap Map headers, @Body SubmitPaymentReturnParametersRequest request); } diff --git a/src/main/java/com/truelayer/java/commonapi/ICommonHandler.java b/src/main/java/com/truelayer/java/commonapi/ICommonHandler.java new file mode 100644 index 00000000..7ad0611c --- /dev/null +++ b/src/main/java/com/truelayer/java/commonapi/ICommonHandler.java @@ -0,0 +1,16 @@ +package com.truelayer.java.commonapi; + +import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; +import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; +import java.util.concurrent.CompletableFuture; + +public interface ICommonHandler { + + CompletableFuture> submitPaymentReturnParameters( + SubmitPaymentReturnParametersRequest request); + + CompletableFuture> submitPaymentReturnParameters( + Headers headers, SubmitPaymentReturnParametersRequest request); +} diff --git a/src/main/java/com/truelayer/java/entities/EmptyRequestBody.java b/src/main/java/com/truelayer/java/entities/EmptyRequestBody.java new file mode 100644 index 00000000..c5e5c7f1 --- /dev/null +++ b/src/main/java/com/truelayer/java/entities/EmptyRequestBody.java @@ -0,0 +1,6 @@ +package com.truelayer.java.entities; + +import lombok.Value; + +@Value +public class EmptyRequestBody {} diff --git a/src/main/java/com/truelayer/java/http/OkHttpClientFactory.java b/src/main/java/com/truelayer/java/http/OkHttpClientFactory.java index 29aa2adb..3acb166f 100644 --- a/src/main/java/com/truelayer/java/http/OkHttpClientFactory.java +++ b/src/main/java/com/truelayer/java/http/OkHttpClientFactory.java @@ -9,10 +9,7 @@ import com.truelayer.java.http.auth.AccessTokenInvalidator; import com.truelayer.java.http.auth.AccessTokenManager; import com.truelayer.java.http.auth.cache.ICredentialsCache; -import com.truelayer.java.http.interceptors.AuthenticationInterceptor; -import com.truelayer.java.http.interceptors.IdempotencyKeyInterceptor; -import com.truelayer.java.http.interceptors.SignatureInterceptor; -import com.truelayer.java.http.interceptors.TrueLayerAgentInterceptor; +import com.truelayer.java.http.interceptors.*; import com.truelayer.java.http.interceptors.logging.HttpLoggingInterceptor; import com.truelayer.java.http.interceptors.logging.SensitiveHeaderGuard; import com.truelayer.java.versioninfo.LibraryInfoLoader; @@ -105,7 +102,7 @@ public OkHttpClient buildAuthApiClient(OkHttpClient baseHttpClient, ClientCreden OkHttpClient.Builder clientBuilder = baseHttpClient.newBuilder(); - clientBuilder.addInterceptor(new IdempotencyKeyInterceptor()); + clientBuilder.addInterceptor(new IdempotencyKeyGeneratorInterceptor()); return clientBuilder.build(); } @@ -120,7 +117,7 @@ public OkHttpClient buildPaymentsApiClient( // as all the others are inherited OkHttpClient.Builder paymentsHttpClientBuilder = authApiHttpClient.newBuilder(); - paymentsHttpClientBuilder.addInterceptor(new SignatureInterceptor(signingOptions)); + paymentsHttpClientBuilder.addInterceptor(new SignatureGeneratorInterceptor(signingOptions)); AccessTokenManager.AccessTokenManagerBuilder accessTokenManagerBuilder = AccessTokenManager.builder().authenticationHandler(authenticationHandler); diff --git a/src/main/java/com/truelayer/java/http/entities/Header.java b/src/main/java/com/truelayer/java/http/entities/Header.java deleted file mode 100644 index f40d0716..00000000 --- a/src/main/java/com/truelayer/java/http/entities/Header.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.truelayer.java.http.entities; - -import lombok.Getter; -import lombok.Value; -import lombok.experimental.Accessors; - -@Value -@Getter -@Accessors(fluent = true) -public class Header { - String name; - String value; - - @Override - public String toString() { - return this.name + "=" + value; - } -} diff --git a/src/main/java/com/truelayer/java/http/entities/Headers.java b/src/main/java/com/truelayer/java/http/entities/Headers.java new file mode 100644 index 00000000..fb9745ee --- /dev/null +++ b/src/main/java/com/truelayer/java/http/entities/Headers.java @@ -0,0 +1,21 @@ +package com.truelayer.java.http.entities; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** + * Class representing custom TL headers that we allow our users to set. + */ +@Builder +@Getter +@ToString +@EqualsAndHashCode +public class Headers { + private String idempotencyKey; + + private String signature; + + private String xForwardedFor; +} diff --git a/src/main/java/com/truelayer/java/http/interceptors/IdempotencyKeyInterceptor.java b/src/main/java/com/truelayer/java/http/interceptors/IdempotencyKeyGeneratorInterceptor.java similarity index 58% rename from src/main/java/com/truelayer/java/http/interceptors/IdempotencyKeyInterceptor.java rename to src/main/java/com/truelayer/java/http/interceptors/IdempotencyKeyGeneratorInterceptor.java index fd0f185c..42aa3c44 100644 --- a/src/main/java/com/truelayer/java/http/interceptors/IdempotencyKeyInterceptor.java +++ b/src/main/java/com/truelayer/java/http/interceptors/IdempotencyKeyGeneratorInterceptor.java @@ -1,5 +1,7 @@ package com.truelayer.java.http.interceptors; +import static org.apache.commons.lang3.ObjectUtils.isEmpty; + import com.truelayer.java.Constants; import java.io.IOException; import java.util.UUID; @@ -7,14 +9,23 @@ import okhttp3.Request; import okhttp3.Response; -public class IdempotencyKeyInterceptor implements Interceptor { +public class IdempotencyKeyGeneratorInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); + + if (!needsIdempotencyGeneration(request)) { + return chain.proceed(request); + } + Request newRequest = request.newBuilder() .header(Constants.HeaderNames.IDEMPOTENCY_KEY, UUID.randomUUID().toString()) .build(); return chain.proceed(newRequest); } + + private boolean needsIdempotencyGeneration(Request request) { + return isEmpty(request.header(Constants.HeaderNames.IDEMPOTENCY_KEY)); + } } diff --git a/src/main/java/com/truelayer/java/http/interceptors/SignatureInterceptor.java b/src/main/java/com/truelayer/java/http/interceptors/SignatureGeneratorInterceptor.java similarity index 66% rename from src/main/java/com/truelayer/java/http/interceptors/SignatureInterceptor.java rename to src/main/java/com/truelayer/java/http/interceptors/SignatureGeneratorInterceptor.java index b79efe9f..3bebefb4 100644 --- a/src/main/java/com/truelayer/java/http/interceptors/SignatureInterceptor.java +++ b/src/main/java/com/truelayer/java/http/interceptors/SignatureGeneratorInterceptor.java @@ -15,7 +15,7 @@ import okio.Buffer; @RequiredArgsConstructor -public class SignatureInterceptor implements Interceptor { +public class SignatureGeneratorInterceptor implements Interceptor { private final SigningOptions signingOptions; @@ -23,20 +23,20 @@ public class SignatureInterceptor implements Interceptor { public Response intercept(Chain chain) throws IOException { Request request = chain.request(); - if (needsSignature(request)) { - Request clonedRequest = request.newBuilder().build(); - - String signature = computeSignature( - clonedRequest.method().toLowerCase(), - clonedRequest.url().encodedPath(), - clonedRequest.header(IDEMPOTENCY_KEY), - getBodyAsString(clonedRequest)); - Request newRequest = - request.newBuilder().header(TL_SIGNATURE, signature).build(); - return chain.proceed(newRequest); + if (!needsSignatureGeneration(request)) { + return chain.proceed(request); } - return chain.proceed(request); + Request clonedRequest = request.newBuilder().build(); + + String signature = computeSignature( + clonedRequest.method().toLowerCase(), + clonedRequest.url().encodedPath(), + clonedRequest.header(IDEMPOTENCY_KEY), + getBodyAsString(clonedRequest)); + Request newRequest = + request.newBuilder().header(TL_SIGNATURE, signature).build(); + return chain.proceed(newRequest); } private String getBodyAsString(Request request) throws IOException { @@ -50,8 +50,8 @@ private String getBodyAsString(Request request) throws IOException { } } - private boolean needsSignature(Request request) { - return !request.method().equalsIgnoreCase("get"); + private boolean needsSignatureGeneration(Request request) { + return !request.method().equalsIgnoreCase("get") && isEmpty(request.header(TL_SIGNATURE)); } private String computeSignature(String method, String path, String idempotencyKey, String jsonBody) { diff --git a/src/main/java/com/truelayer/java/http/interceptors/logging/HttpLogMessage.java b/src/main/java/com/truelayer/java/http/interceptors/logging/HttpLogMessage.java index 14b1679e..5a4e52a7 100644 --- a/src/main/java/com/truelayer/java/http/interceptors/logging/HttpLogMessage.java +++ b/src/main/java/com/truelayer/java/http/interceptors/logging/HttpLogMessage.java @@ -2,9 +2,9 @@ import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; -import com.truelayer.java.http.entities.Header; import java.util.List; import lombok.Builder; +import okhttp3.internal.http2.Header; @Builder public class HttpLogMessage { diff --git a/src/main/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuard.java b/src/main/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuard.java index 5b8dc855..da8e54a7 100644 --- a/src/main/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuard.java +++ b/src/main/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuard.java @@ -3,11 +3,11 @@ import static org.apache.commons.lang3.ObjectUtils.isEmpty; import com.truelayer.java.Constants; -import com.truelayer.java.http.entities.Header; import java.util.ArrayList; import java.util.Collections; import java.util.List; import okhttp3.Headers; +import okhttp3.internal.http2.Header; public class SensitiveHeaderGuard { diff --git a/src/main/java/com/truelayer/java/http/mappers/ErrorMapper.java b/src/main/java/com/truelayer/java/http/mappers/ErrorMapper.java index 237b209e..e3499b4a 100644 --- a/src/main/java/com/truelayer/java/http/mappers/ErrorMapper.java +++ b/src/main/java/com/truelayer/java/http/mappers/ErrorMapper.java @@ -16,7 +16,7 @@ public class ErrorMapper { public static final String GENERIC_ERROR_TITLE = "server_error"; public static final String GENERIC_ERROR_TYPE = "https://docs.truelayer.com/docs/error-types"; - public ProblemDetails toProblemDetails(Response response) { + public static ProblemDetails toProblemDetails(Response response) { final String correlationId = tryGetCorrelationId(response.headers()); final ResponseBody errorBody = response.errorBody(); @@ -43,7 +43,7 @@ public ProblemDetails toProblemDetails(Response response) { : tryMapLegacyError(response.code(), correlationId, errorBodyString); } - private ProblemDetails tryMapLegacyError(int code, String correlationId, String errorBody) { + private static ProblemDetails tryMapLegacyError(int code, String correlationId, String errorBody) { try { LegacyError legacyError = getObjectMapper().readValue(errorBody, LegacyError.class); return buildFallbackError(code, correlationId, legacyError.getError()); @@ -52,7 +52,7 @@ private ProblemDetails tryMapLegacyError(int code, String correlationId, String } } - private ProblemDetails buildFallbackError(int code, String correlationId, String title) { + private static ProblemDetails buildFallbackError(int code, String correlationId, String title) { return ProblemDetails.builder() .status(code) .title(title) @@ -61,7 +61,7 @@ private ProblemDetails buildFallbackError(int code, String correlationId, String .build(); } - private String tryGetCorrelationId(Headers headers) { + private static String tryGetCorrelationId(Headers headers) { if (ObjectUtils.isEmpty(headers)) { return null; } diff --git a/src/main/java/com/truelayer/java/http/mappers/HeadersMapper.java b/src/main/java/com/truelayer/java/http/mappers/HeadersMapper.java new file mode 100644 index 00000000..ab9f35ba --- /dev/null +++ b/src/main/java/com/truelayer/java/http/mappers/HeadersMapper.java @@ -0,0 +1,40 @@ +package com.truelayer.java.http.mappers; + +import static org.apache.commons.lang3.ObjectUtils.isEmpty; +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; + +import com.truelayer.java.Constants; +import com.truelayer.java.http.entities.Headers; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class for Headers to Map conversions. + */ +public class HeadersMapper { + public static Map toMap(Headers customHeaders) { + Map headersMap = new HashMap<>(); + + if (isEmpty(customHeaders)) { + return Collections.unmodifiableMap(headersMap); + } + + String idempotencyKey = customHeaders.getIdempotencyKey(); + if (isNotEmpty(idempotencyKey)) { + headersMap.put(Constants.HeaderNames.IDEMPOTENCY_KEY, idempotencyKey); + } + + String signature = customHeaders.getSignature(); + if (isNotEmpty(signature)) { + headersMap.put(Constants.HeaderNames.TL_SIGNATURE, signature); + } + + String xForwardedFor = customHeaders.getXForwardedFor(); + if (isNotEmpty(xForwardedFor)) { + headersMap.put(Constants.HeaderNames.X_FORWARDED_FOR, xForwardedFor); + } + + return Collections.unmodifiableMap(headersMap); + } +} diff --git a/src/main/java/com/truelayer/java/mandates/IMandatesApi.java b/src/main/java/com/truelayer/java/mandates/IMandatesApi.java index b914634b..ffd2e568 100644 --- a/src/main/java/com/truelayer/java/mandates/IMandatesApi.java +++ b/src/main/java/com/truelayer/java/mandates/IMandatesApi.java @@ -6,6 +6,7 @@ import com.truelayer.java.payments.entities.AuthorizationFlowResponse; import com.truelayer.java.payments.entities.StartAuthorizationFlowRequest; import com.truelayer.java.payments.entities.SubmitProviderSelectionRequest; +import java.util.Map; import java.util.concurrent.CompletableFuture; import retrofit2.http.*; @@ -18,15 +19,18 @@ public interface IMandatesApi { /** * Create a new mandate + * @param headers map representing custom HTTP headers to be sent * @param request the create mandate request * @return the created mandate * @see Create mandate API reference */ @POST("/mandates") - CompletableFuture> createMandate(@Body CreateMandateRequest request); + CompletableFuture> createMandate( + @HeaderMap Map headers, @Body CreateMandateRequest request); /** * Start the authorization flow for a mandate. + * @param headers map representing custom HTTP headers to be sent * @param mandateId the id of the mandate * @param request the start authorization flow request * @return the mandate authorization flow created @@ -34,10 +38,13 @@ public interface IMandatesApi { */ @POST("/mandates/{id}/authorization-flow") CompletableFuture> startAuthorizationFlow( - @Path("id") String mandateId, @Body StartAuthorizationFlowRequest request); + @HeaderMap Map headers, + @Path("id") String mandateId, + @Body StartAuthorizationFlowRequest request); /** * Submit the provider details selected by the PSU + * @param headers map representing custom HTTP headers to be sent * @param mandateId the id of the mandate * @param request the provider selection request * @return the next action to take care of @@ -45,7 +52,9 @@ CompletableFuture> startAuthorizationFlow */ @POST("/mandates/{id}/authorization-flow/actions/provider-selection") CompletableFuture> submitProviderSelection( - @Path("id") String mandateId, @Body SubmitProviderSelectionRequest request); + @HeaderMap Map headers, + @Path("id") String mandateId, + @Body SubmitProviderSelectionRequest request); /** * List all the mandates associated to the client used @@ -70,12 +79,14 @@ CompletableFuture> listMandates( /** * Revoke mandate + * @param headers map representing custom HTTP headers to be sent * @param mandateId the id of the mandate * @return an empty response in case of success * @see Revoke mandate API reference */ @POST("/mandates/{id}/revoke") - CompletableFuture> revokeMandate(@Path("id") String mandateId); + CompletableFuture> revokeMandate( + @HeaderMap Map headers, @Path("id") String mandateId); /** * Get Confirmation Of Funds diff --git a/src/main/java/com/truelayer/java/mandates/IMandatesHandler.java b/src/main/java/com/truelayer/java/mandates/IMandatesHandler.java index 433bebbe..3e68348f 100644 --- a/src/main/java/com/truelayer/java/mandates/IMandatesHandler.java +++ b/src/main/java/com/truelayer/java/mandates/IMandatesHandler.java @@ -1,6 +1,7 @@ package com.truelayer.java.mandates; import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.mandates.entities.*; import com.truelayer.java.mandates.entities.mandatedetail.MandateDetail; import com.truelayer.java.payments.entities.AuthorizationFlowResponse; @@ -15,12 +16,20 @@ public interface IMandatesHandler { CompletableFuture> createMandate(CreateMandateRequest request); + CompletableFuture> createMandate(Headers headers, CreateMandateRequest request); + CompletableFuture> startAuthorizationFlow( String mandateId, StartAuthorizationFlowRequest request); + CompletableFuture> startAuthorizationFlow( + Headers headers, String mandateId, StartAuthorizationFlowRequest request); + CompletableFuture> submitProviderSelection( String mandateId, SubmitProviderSelectionRequest request); + CompletableFuture> submitProviderSelection( + Headers headers, String mandateId, SubmitProviderSelectionRequest request); + CompletableFuture> listMandates(); CompletableFuture> listMandates(ListMandatesQuery query); @@ -29,6 +38,8 @@ CompletableFuture> submitProviderSelectio CompletableFuture> revokeMandate(String mandateId); + CompletableFuture> revokeMandate(Headers headers, String mandateId); + CompletableFuture> getConfirmationOfFunds( String mandateId, String amount, String currency); diff --git a/src/main/java/com/truelayer/java/mandates/MandatesHandler.java b/src/main/java/com/truelayer/java/mandates/MandatesHandler.java index 9b210e4a..499642ec 100644 --- a/src/main/java/com/truelayer/java/mandates/MandatesHandler.java +++ b/src/main/java/com/truelayer/java/mandates/MandatesHandler.java @@ -1,6 +1,10 @@ package com.truelayer.java.mandates; +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; + import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.mandates.entities.*; import com.truelayer.java.mandates.entities.mandatedetail.MandateDetail; import com.truelayer.java.payments.entities.AuthorizationFlowResponse; @@ -15,19 +19,37 @@ public class MandatesHandler implements IMandatesHandler { @Override public CompletableFuture> createMandate(CreateMandateRequest request) { - return mandatesApi.createMandate(request); + return mandatesApi.createMandate(emptyMap(), request); + } + + @Override + public CompletableFuture> createMandate( + Headers headers, CreateMandateRequest request) { + return mandatesApi.createMandate(toMap(headers), request); } @Override public CompletableFuture> startAuthorizationFlow( String mandateId, StartAuthorizationFlowRequest request) { - return mandatesApi.startAuthorizationFlow(mandateId, request); + return mandatesApi.startAuthorizationFlow(emptyMap(), mandateId, request); + } + + @Override + public CompletableFuture> startAuthorizationFlow( + Headers headers, String mandateId, StartAuthorizationFlowRequest request) { + return mandatesApi.startAuthorizationFlow(toMap(headers), mandateId, request); } @Override public CompletableFuture> submitProviderSelection( String mandateId, SubmitProviderSelectionRequest request) { - return mandatesApi.submitProviderSelection(mandateId, request); + return mandatesApi.submitProviderSelection(emptyMap(), mandateId, request); + } + + @Override + public CompletableFuture> submitProviderSelection( + Headers headers, String mandateId, SubmitProviderSelectionRequest request) { + return mandatesApi.submitProviderSelection(toMap(headers), mandateId, request); } @Override @@ -47,7 +69,12 @@ public CompletableFuture> getMandate(String mandateId @Override public CompletableFuture> revokeMandate(String mandateId) { - return mandatesApi.revokeMandate(mandateId); + return mandatesApi.revokeMandate(emptyMap(), mandateId); + } + + @Override + public CompletableFuture> revokeMandate(Headers headers, String mandateId) { + return mandatesApi.revokeMandate(toMap(headers), mandateId); } @Override diff --git a/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsApi.java b/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsApi.java index 3c88e5df..743858a7 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsApi.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsApi.java @@ -4,6 +4,7 @@ import com.truelayer.java.merchantaccounts.entities.*; import com.truelayer.java.merchantaccounts.entities.sweeping.SweepingSettings; import com.truelayer.java.merchantaccounts.entities.transactions.TransactionTypeQuery; +import java.util.Map; import java.util.concurrent.CompletableFuture; import retrofit2.http.*; import retrofit2.http.GET; @@ -54,6 +55,7 @@ CompletableFuture> listTransactions( /** * Set the automatic sweeping settings for a merchant account. At regular intervals, any available balance in excess * of the configured max_amount_in_minor is withdrawn to a pre-configured IBAN. + * @param headers map representing custom HTTP headers to be sent * @param merchantAccountId the id of the merchant account * @param updateSweepingRequest the update/setup sweeping request * @return the updated sweeping settings @@ -61,7 +63,9 @@ CompletableFuture> listTransactions( */ @POST("/merchant-accounts/{merchantAccountId}/sweeping") CompletableFuture> updateSweeping( - @Path("merchantAccountId") String merchantAccountId, @Body UpdateSweepingRequest updateSweepingRequest); + @HeaderMap Map headers, + @Path("merchantAccountId") String merchantAccountId, + @Body UpdateSweepingRequest updateSweepingRequest); /** * Get the automatic sweeping settings for a merchant account. @@ -75,12 +79,14 @@ CompletableFuture> getSweepingSettings( /** * Disable automatic sweeping for a merchant account. + * @param headers map representing custom HTTP headers to be sent * @param merchantAccountId the id of the merchant account * @return an empty response in case of success * @see Disable Sweeping API reference */ @DELETE("/merchant-accounts/{merchantAccountId}/sweeping") - CompletableFuture> disableSweeping(@Path("merchantAccountId") String merchantAccountId); + CompletableFuture> disableSweeping( + @HeaderMap Map headers, @Path("merchantAccountId") String merchantAccountId); @GET("/merchant-accounts/{merchantAccountId}/payment-sources") CompletableFuture> listPaymentSources( diff --git a/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsHandler.java b/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsHandler.java index ac6f6402..7e4e3a92 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsHandler.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/IMerchantAccountsHandler.java @@ -1,6 +1,7 @@ package com.truelayer.java.merchantaccounts; import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.merchantaccounts.entities.*; import com.truelayer.java.merchantaccounts.entities.sweeping.SweepingSettings; import java.util.concurrent.CompletableFuture; @@ -18,10 +19,15 @@ CompletableFuture> listTransactions( CompletableFuture> updateSweeping( String merchantAccountId, UpdateSweepingRequest updateSweepingRequest); + CompletableFuture> updateSweeping( + Headers headers, String merchantAccountId, UpdateSweepingRequest updateSweepingRequest); + CompletableFuture> getSweepingSettings(String merchantAccountId); CompletableFuture> disableSweeping(String merchantAccountId); + CompletableFuture> disableSweeping(Headers headers, String merchantAccountId); + CompletableFuture> listPaymentSources( String merchantAccountId, ListPaymentSourcesQuery query); } diff --git a/src/main/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandler.java b/src/main/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandler.java index dd0ece7a..7ecc3d87 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandler.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandler.java @@ -1,6 +1,10 @@ package com.truelayer.java.merchantaccounts; +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; + import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.merchantaccounts.entities.*; import com.truelayer.java.merchantaccounts.entities.sweeping.SweepingSettings; import java.time.format.DateTimeFormatter; @@ -35,7 +39,13 @@ public CompletableFuture> listTransactions @Override public CompletableFuture> updateSweeping( String merchantAccountId, UpdateSweepingRequest updateSweepingRequest) { - return merchantAccountsApi.updateSweeping(merchantAccountId, updateSweepingRequest); + return merchantAccountsApi.updateSweeping(emptyMap(), merchantAccountId, updateSweepingRequest); + } + + @Override + public CompletableFuture> updateSweeping( + Headers headers, String merchantAccountId, UpdateSweepingRequest updateSweepingRequest) { + return merchantAccountsApi.updateSweeping(toMap(headers), merchantAccountId, updateSweepingRequest); } @Override @@ -45,7 +55,12 @@ public CompletableFuture> getSweepingSettings(Stri @Override public CompletableFuture> disableSweeping(String merchantAccountId) { - return merchantAccountsApi.disableSweeping(merchantAccountId); + return merchantAccountsApi.disableSweeping(emptyMap(), merchantAccountId); + } + + @Override + public CompletableFuture> disableSweeping(Headers headers, String merchantAccountId) { + return merchantAccountsApi.disableSweeping(toMap(headers), merchantAccountId); } @Override diff --git a/src/main/java/com/truelayer/java/payments/IPaymentsApi.java b/src/main/java/com/truelayer/java/payments/IPaymentsApi.java index 6a02175e..ac387ca2 100644 --- a/src/main/java/com/truelayer/java/payments/IPaymentsApi.java +++ b/src/main/java/com/truelayer/java/payments/IPaymentsApi.java @@ -1,11 +1,11 @@ package com.truelayer.java.payments; -import static com.truelayer.java.Constants.HeaderNames.X_FORWARDED_FOR; - +import com.truelayer.java.entities.EmptyRequestBody; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.payments.entities.*; import com.truelayer.java.payments.entities.paymentdetail.PaymentDetail; import com.truelayer.java.payments.entities.paymentrefund.PaymentRefund; +import java.util.Map; import java.util.concurrent.CompletableFuture; import retrofit2.http.*; @@ -15,15 +15,16 @@ * @see Payments API reference */ public interface IPaymentsApi { - /** * Initialises a payment resource. + * @param headers map representing custom HTTP headers to be sent * @param request a create payment request payload * @return the response of the Create Payment operation * @see Create Payment API reference */ @POST("/payments") - CompletableFuture> createPayment(@Body CreatePaymentRequest request); + CompletableFuture> createPayment( + @HeaderMap Map headers, @Body CreatePaymentRequest request); /** * Gets a payment resource by id. @@ -36,6 +37,7 @@ public interface IPaymentsApi { /** * Starts an authorization flow for a given payment resource. + * @param headers map representing custom HTTP headers to be sent * @param paymentId the payment identifier * @param request a start authorization flow request payload * @return the response of the Start Authorization Flow operation @@ -43,25 +45,13 @@ public interface IPaymentsApi { */ @POST("/payments/{id}/authorization-flow") CompletableFuture> startAuthorizationFlow( - @Path("id") String paymentId, @Body StartAuthorizationFlowRequest request); - - /** - * Starts an authorization flow for a given payment resource, - * including the X-Forwarded-For HTTP header field to record the end-user IP address. - * @param paymentId the payment identifier - * @param request a start authorization flow request payload - * @param xForwardedFor the end-user IP address - * @return the response of the Start Authorization Flow operation - * @see Start Authorization Flow API reference - */ - @POST("/payments/{id}/authorization-flow") - CompletableFuture> startAuthorizationFlow( + @HeaderMap Map headers, @Path("id") String paymentId, - @Body StartAuthorizationFlowRequest request, - @Header(X_FORWARDED_FOR) String xForwardedFor); + @Body StartAuthorizationFlowRequest request); /** * Submit the provider selection for a given payment resource. + * @param headers map representing custom HTTP headers to be sent * @param paymentId the payment identifier * @param request a submit provider selection request payload * @return the response of the Submit Provider Selection operation @@ -69,10 +59,13 @@ CompletableFuture> startAuthorizationFlow */ @POST("/payments/{id}/authorization-flow/actions/provider-selection") CompletableFuture> submitProviderSelection( - @Path("id") String paymentId, @Body SubmitProviderSelectionRequest request); + @HeaderMap Map headers, + @Path("id") String paymentId, + @Body SubmitProviderSelectionRequest request); /** * Submit consent collected from the PSU for a given payment resource. + * @param headers map representing custom HTTP headers to be sent * @param paymentId the payment identifier * @param request a submit consent request payload * @return the response of the Submit Consent operation @@ -80,10 +73,11 @@ CompletableFuture> submitProviderSelectio */ @POST("/payments/{id}/authorization-flow/actions/consent") CompletableFuture> submitConsent( - @Path("id") String paymentId, @Body SubmitConsentRequest request); + @HeaderMap Map headers, @Path("id") String paymentId, @Body EmptyRequestBody request); /** * Submit form inputs collected from the PSU for a given payment resource. + * @param headers map representing custom HTTP headers to be sent * @param paymentId the payment identifier * @param request a submit form request payload * @return the response of the Submit Form operation @@ -91,10 +85,11 @@ CompletableFuture> submitConsent( */ @POST("/payments/{id}/authorization-flow/actions/form") CompletableFuture> submitForm( - @Path("id") String paymentId, @Body SubmitFormRequest request); + @HeaderMap Map headers, @Path("id") String paymentId, @Body SubmitFormRequest request); /** * Refund a merchant account payment. + * @param headers map representing custom HTTP headers to be sent * @param paymentId the payment identifier * @param request a create refund request payload * @return the response of the Create Payment Refund operation @@ -102,7 +97,9 @@ CompletableFuture> submitForm( */ @POST("/payments/{id}/refunds") CompletableFuture> createPaymentRefund( - @Path("id") String paymentId, @Body CreatePaymentRefundRequest request); + @HeaderMap Map headers, + @Path("id") String paymentId, + @Body CreatePaymentRefundRequest request); /** * Returns all refunds of a payment. diff --git a/src/main/java/com/truelayer/java/payments/IPaymentsHandler.java b/src/main/java/com/truelayer/java/payments/IPaymentsHandler.java new file mode 100644 index 00000000..36a8c4e1 --- /dev/null +++ b/src/main/java/com/truelayer/java/payments/IPaymentsHandler.java @@ -0,0 +1,52 @@ +package com.truelayer.java.payments; + +import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; +import com.truelayer.java.payments.entities.*; +import com.truelayer.java.payments.entities.paymentdetail.PaymentDetail; +import com.truelayer.java.payments.entities.paymentrefund.PaymentRefund; +import java.util.concurrent.CompletableFuture; + +/** + * Provides /payments API integration without the burden of Retrofit's annotation + * and improve both usability and backward compatibility for the implemented endpoints. + */ +public interface IPaymentsHandler { + + CompletableFuture> createPayment(CreatePaymentRequest request); + + CompletableFuture> createPayment(Headers headers, CreatePaymentRequest request); + + CompletableFuture> getPayment(String paymentId); + + CompletableFuture> startAuthorizationFlow( + String paymentId, StartAuthorizationFlowRequest request); + + CompletableFuture> startAuthorizationFlow( + Headers headers, String paymentId, StartAuthorizationFlowRequest request); + + CompletableFuture> submitProviderSelection( + String paymentId, SubmitProviderSelectionRequest request); + + CompletableFuture> submitProviderSelection( + Headers headers, String paymentId, SubmitProviderSelectionRequest request); + + CompletableFuture> submitConsent(String paymentId); + + CompletableFuture> submitConsent(Headers headers, String paymentId); + + CompletableFuture> submitForm(String paymentId, SubmitFormRequest request); + + CompletableFuture> submitForm( + Headers headers, String paymentId, SubmitFormRequest request); + + CompletableFuture> createPaymentRefund( + String paymentId, CreatePaymentRefundRequest request); + + CompletableFuture> createPaymentRefund( + Headers headers, String paymentId, CreatePaymentRefundRequest request); + + CompletableFuture> listPaymentRefunds(String paymentId); + + CompletableFuture> getPaymentRefundById(String paymentId, String refundId); +} diff --git a/src/main/java/com/truelayer/java/payments/PaymentsHandler.java b/src/main/java/com/truelayer/java/payments/PaymentsHandler.java new file mode 100644 index 00000000..8c31d214 --- /dev/null +++ b/src/main/java/com/truelayer/java/payments/PaymentsHandler.java @@ -0,0 +1,103 @@ +package com.truelayer.java.payments; + +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; + +import com.truelayer.java.entities.EmptyRequestBody; +import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; +import com.truelayer.java.payments.entities.*; +import com.truelayer.java.payments.entities.paymentdetail.PaymentDetail; +import com.truelayer.java.payments.entities.paymentrefund.PaymentRefund; +import java.util.concurrent.CompletableFuture; +import lombok.Value; + +@Value +public class PaymentsHandler implements IPaymentsHandler { + + IPaymentsApi paymentsApi; + + @Override + public CompletableFuture> createPayment(CreatePaymentRequest request) { + return paymentsApi.createPayment(emptyMap(), request); + } + + @Override + public CompletableFuture> createPayment( + Headers headers, CreatePaymentRequest request) { + return paymentsApi.createPayment(toMap(headers), request); + } + + @Override + public CompletableFuture> getPayment(String paymentId) { + return paymentsApi.getPayment(paymentId); + } + + @Override + public CompletableFuture> startAuthorizationFlow( + String paymentId, StartAuthorizationFlowRequest request) { + return paymentsApi.startAuthorizationFlow(emptyMap(), paymentId, request); + } + + @Override + public CompletableFuture> startAuthorizationFlow( + Headers headers, String paymentId, StartAuthorizationFlowRequest request) { + return paymentsApi.startAuthorizationFlow(toMap(headers), paymentId, request); + } + + @Override + public CompletableFuture> submitProviderSelection( + String paymentId, SubmitProviderSelectionRequest request) { + return paymentsApi.submitProviderSelection(emptyMap(), paymentId, request); + } + + @Override + public CompletableFuture> submitProviderSelection( + Headers headers, String paymentId, SubmitProviderSelectionRequest request) { + return paymentsApi.submitProviderSelection(toMap(headers), paymentId, request); + } + + @Override + public CompletableFuture> submitConsent(String paymentId) { + return paymentsApi.submitConsent(emptyMap(), paymentId, new EmptyRequestBody()); + } + + @Override + public CompletableFuture> submitConsent(Headers headers, String paymentId) { + return paymentsApi.submitConsent(toMap(headers), paymentId, new EmptyRequestBody()); + } + + @Override + public CompletableFuture> submitForm( + String paymentId, SubmitFormRequest request) { + return paymentsApi.submitForm(emptyMap(), paymentId, request); + } + + @Override + public CompletableFuture> submitForm( + Headers headers, String paymentId, SubmitFormRequest request) { + return paymentsApi.submitForm(toMap(headers), paymentId, request); + } + + @Override + public CompletableFuture> createPaymentRefund( + String paymentId, CreatePaymentRefundRequest request) { + return paymentsApi.createPaymentRefund(emptyMap(), paymentId, request); + } + + @Override + public CompletableFuture> createPaymentRefund( + Headers headers, String paymentId, CreatePaymentRefundRequest request) { + return paymentsApi.createPaymentRefund(toMap(headers), paymentId, request); + } + + @Override + public CompletableFuture> listPaymentRefunds(String paymentId) { + return paymentsApi.listPaymentRefunds(paymentId); + } + + @Override + public CompletableFuture> getPaymentRefundById(String paymentId, String refundId) { + return paymentsApi.getPaymentRefundById(paymentId, refundId); + } +} diff --git a/src/main/java/com/truelayer/java/payments/entities/SubmitConsentRequest.java b/src/main/java/com/truelayer/java/payments/entities/SubmitConsentRequest.java deleted file mode 100644 index ce8797a2..00000000 --- a/src/main/java/com/truelayer/java/payments/entities/SubmitConsentRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.truelayer.java.payments.entities; - -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; - -@Builder -@Getter -@ToString -@EqualsAndHashCode -public class SubmitConsentRequest {} diff --git a/src/main/java/com/truelayer/java/payouts/IPayoutsApi.java b/src/main/java/com/truelayer/java/payouts/IPayoutsApi.java index 5296c874..fa89be26 100644 --- a/src/main/java/com/truelayer/java/payouts/IPayoutsApi.java +++ b/src/main/java/com/truelayer/java/payouts/IPayoutsApi.java @@ -4,22 +4,22 @@ import com.truelayer.java.payouts.entities.CreatePayoutRequest; import com.truelayer.java.payouts.entities.CreatePayoutResponse; import com.truelayer.java.payouts.entities.Payout; +import java.util.Map; import java.util.concurrent.CompletableFuture; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.POST; -import retrofit2.http.Path; +import retrofit2.http.*; public interface IPayoutsApi { /** * Pay out from one of your merchant accounts. + * @param headers map representing custom HTTP headers to be sent * @param request a create payout request payload * @return the response of the Create Payout operation * @see Create Payout API reference */ @POST("/payouts") - CompletableFuture> createPayout(@Body CreatePayoutRequest request); + CompletableFuture> createPayout( + @HeaderMap Map headers, @Body CreatePayoutRequest request); /** * Returns payout details. diff --git a/src/main/java/com/truelayer/java/payouts/IPayoutsHandler.java b/src/main/java/com/truelayer/java/payouts/IPayoutsHandler.java new file mode 100644 index 00000000..52851e7d --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/IPayoutsHandler.java @@ -0,0 +1,21 @@ +package com.truelayer.java.payouts; + +import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; +import com.truelayer.java.payouts.entities.CreatePayoutRequest; +import com.truelayer.java.payouts.entities.CreatePayoutResponse; +import com.truelayer.java.payouts.entities.Payout; +import java.util.concurrent.CompletableFuture; + +/** + * Provides /payouts API integration without the burden of Retrofit's annotation + * and improve both usability and backward compatibility for the implemented endpoints. + */ +public interface IPayoutsHandler { + + CompletableFuture> createPayout(CreatePayoutRequest request); + + CompletableFuture> createPayout(Headers headers, CreatePayoutRequest request); + + CompletableFuture> getPayout(String payoutId); +} diff --git a/src/main/java/com/truelayer/java/payouts/PayoutsHandler.java b/src/main/java/com/truelayer/java/payouts/PayoutsHandler.java new file mode 100644 index 00000000..a2774de4 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/PayoutsHandler.java @@ -0,0 +1,34 @@ +package com.truelayer.java.payouts; + +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; + +import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; +import com.truelayer.java.payouts.entities.CreatePayoutRequest; +import com.truelayer.java.payouts.entities.CreatePayoutResponse; +import com.truelayer.java.payouts.entities.Payout; +import java.util.concurrent.CompletableFuture; +import lombok.Value; + +@Value +public class PayoutsHandler implements IPayoutsHandler { + + IPayoutsApi payoutsApi; + + @Override + public CompletableFuture> createPayout(CreatePayoutRequest request) { + return payoutsApi.createPayout(emptyMap(), request); + } + + @Override + public CompletableFuture> createPayout( + Headers headers, CreatePayoutRequest request) { + return payoutsApi.createPayout(toMap(headers), request); + } + + @Override + public CompletableFuture> getPayout(String payoutId) { + return payoutsApi.getPayout(payoutId); + } +} diff --git a/src/test/java/com/truelayer/java/TestUtils.java b/src/test/java/com/truelayer/java/TestUtils.java index 20c28175..ae4afa3c 100644 --- a/src/test/java/com/truelayer/java/TestUtils.java +++ b/src/test/java/com/truelayer/java/TestUtils.java @@ -12,22 +12,25 @@ import com.github.tomakehurst.wiremock.stubbing.StubMapping; import com.truelayer.java.auth.entities.AccessToken; import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.versioninfo.VersionInfo; import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; import java.time.Duration; import java.util.UUID; +import java.util.regex.Pattern; import lombok.SneakyThrows; import okhttp3.OkHttpClient; +import org.apache.commons.lang3.RandomStringUtils; public class TestUtils { - + public static final Pattern UUID_REGEX_PATTERN = + Pattern.compile("^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$"); private static final OkHttpClient HTTP_CLIENT_INSTANCE = new OkHttpClient.Builder() .followRedirects(false) .connectTimeout(Duration.ofSeconds(15)) .build(); - private static final String KEYS_LOCATION = "src/test/resources/keys/"; public static final String JSON_RESPONSES_LOCATION = "src/test/resources/__files/"; @@ -95,8 +98,6 @@ public static T deserializeJsonFileTo(String jsonFile, Class type) { public static class RequestStub { private static final String LIBRARY_INFO = "truelayer-java\\/.+"; - private static final String UUID_REGEX_PATTERN = "^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$"; - private boolean withSignature; private boolean withAuthorization; private boolean withIdempotencyKey; @@ -162,15 +163,15 @@ public StubMapping build() { MappingBuilder request = request(method.toUpperCase(), path).withHeader(TL_AGENT, matching(LIBRARY_INFO)); if (withSignature) { - request.withHeader(TL_SIGNATURE, matching(".*")); + request.withHeader(TL_SIGNATURE, matching(".+")); } if (withAuthorization) { - request.withHeader(AUTHORIZATION, matching(".*")); + request.withHeader(AUTHORIZATION, matching(".+")); } if (withIdempotencyKey) { - request.withHeader(IDEMPOTENCY_KEY, matching(UUID_REGEX_PATTERN)); + request.withHeader(IDEMPOTENCY_KEY, matching(".+")); } if (!isEmpty(xForwardedForHeader)) { @@ -196,4 +197,13 @@ public StubMapping build() { public static OkHttpClient getHttpClientInstance() { return HTTP_CLIENT_INSTANCE; } + + public static Headers buildTestHeaders() { + String randomString = RandomStringUtils.random(5, true, true); + return Headers.builder() + .idempotencyKey("a-custom-key_" + randomString) + .signature("a-custom-signature_" + randomString) + .xForwardedFor("1.2.3.4_" + randomString) + .build(); + } } diff --git a/src/test/java/com/truelayer/java/TrueLayerClientTests.java b/src/test/java/com/truelayer/java/TrueLayerClientTests.java index d70f9ef9..defe537e 100644 --- a/src/test/java/com/truelayer/java/TrueLayerClientTests.java +++ b/src/test/java/com/truelayer/java/TrueLayerClientTests.java @@ -8,8 +8,8 @@ import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder; import com.truelayer.java.mandates.IMandatesHandler; import com.truelayer.java.merchantaccounts.IMerchantAccountsHandler; -import com.truelayer.java.payments.IPaymentsApi; -import com.truelayer.java.payouts.IPayoutsApi; +import com.truelayer.java.payments.IPaymentsHandler; +import com.truelayer.java.payouts.IPayoutsHandler; import lombok.SneakyThrows; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -57,7 +57,7 @@ public void itShouldBuildAPaymentClient() { .signingOptions(getSigningOptions()) .build(); - IPaymentsApi paymentsHandler = trueLayerClient.payments(); + IPaymentsHandler paymentsHandler = trueLayerClient.payments(); assertNotNull(paymentsHandler); } @@ -71,8 +71,8 @@ public void itShouldYieldTheSamePaymentHandler() { .signingOptions(getSigningOptions()) .build(); - IPaymentsApi paymentHandler1 = trueLayerClient.payments(); - IPaymentsApi paymentHandler2 = trueLayerClient.payments(); + IPaymentsHandler paymentHandler1 = trueLayerClient.payments(); + IPaymentsHandler paymentHandler2 = trueLayerClient.payments(); assertSame(paymentHandler1, paymentHandler2); } @@ -135,7 +135,7 @@ public void itShouldYieldAPayoutsHandler() { .signingOptions(getSigningOptions()) .build(); - IPayoutsApi payoutsHandler = trueLayerClient.payouts(); + IPayoutsHandler payoutsHandler = trueLayerClient.payouts(); assertNotNull(payoutsHandler); } diff --git a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java index f6af76af..9b558544 100644 --- a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java @@ -171,9 +171,7 @@ public void shouldCompleteARedirectAuthorizationFlowForAPayment() { // Submit consent ApiResponse submitConsentResponse = tlClient.payments() - .submitConsent( - createPaymentResponse.getData().getId(), - SubmitConsentRequest.builder().build()) + .submitConsent(createPaymentResponse.getData().getId()) .get(); assertNotError(submitConsentResponse); @@ -220,9 +218,7 @@ public void shouldCompleteAnEmbeddedAuthorizationFlowForAPayment() { // Submit consent ApiResponse submitConsentResponse = tlClient.payments() - .submitConsent( - createPaymentResponse.getData().getId(), - SubmitConsentRequest.builder().build()) + .submitConsent(createPaymentResponse.getData().getId()) .get(); assertNotError(submitConsentResponse); diff --git a/src/test/java/com/truelayer/java/commonapi/CommonHandlerTests.java b/src/test/java/com/truelayer/java/commonapi/CommonHandlerTests.java new file mode 100644 index 00000000..78d1c2f7 --- /dev/null +++ b/src/test/java/com/truelayer/java/commonapi/CommonHandlerTests.java @@ -0,0 +1,48 @@ +package com.truelayer.java.commonapi; + +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.truelayer.java.TestUtils; +import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; +import com.truelayer.java.http.entities.Headers; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class CommonHandlerTests { + + @Test + @DisplayName("It should call the submit payment return parameters endpoint") + public void shouldCallSubmitPaymentReturnParamsEndpoint() { + ICommonApi commonApi = Mockito.mock(ICommonApi.class); + CommonHandler sut = new CommonHandler(commonApi); + SubmitPaymentReturnParametersRequest request = SubmitPaymentReturnParametersRequest.builder() + .query("a-query") + .fragment("a-fragment") + .build(); + + sut.submitPaymentReturnParameters(request); + + verify(commonApi, times(1)).submitPaymentReturnParameters(emptyMap(), request); + } + + @Test + @DisplayName("It should call the submit payment return parameters endpoint with additional headers") + public void shouldCallSubmitPaymentReturnParamsEndpointWithCustomHeaders() { + ICommonApi commonApi = Mockito.mock(ICommonApi.class); + CommonHandler sut = new CommonHandler(commonApi); + Headers customHeaders = TestUtils.buildTestHeaders(); + SubmitPaymentReturnParametersRequest request = SubmitPaymentReturnParametersRequest.builder() + .query("a-query") + .fragment("a-fragment") + .build(); + + sut.submitPaymentReturnParameters(customHeaders, request); + + verify(commonApi, times(1)).submitPaymentReturnParameters(toMap(customHeaders), request); + } +} diff --git a/src/test/java/com/truelayer/java/http/OkHttpClientFactoryTests.java b/src/test/java/com/truelayer/java/http/OkHttpClientFactoryTests.java index 6e49feb2..5478586e 100644 --- a/src/test/java/com/truelayer/java/http/OkHttpClientFactoryTests.java +++ b/src/test/java/com/truelayer/java/http/OkHttpClientFactoryTests.java @@ -13,8 +13,8 @@ import com.truelayer.java.auth.IAuthenticationHandler; import com.truelayer.java.http.auth.cache.SimpleCredentialsCache; import com.truelayer.java.http.interceptors.AuthenticationInterceptor; -import com.truelayer.java.http.interceptors.IdempotencyKeyInterceptor; -import com.truelayer.java.http.interceptors.SignatureInterceptor; +import com.truelayer.java.http.interceptors.IdempotencyKeyGeneratorInterceptor; +import com.truelayer.java.http.interceptors.SignatureGeneratorInterceptor; import com.truelayer.java.http.interceptors.TrueLayerAgentInterceptor; import com.truelayer.java.http.interceptors.logging.HttpLoggingInterceptor; import com.truelayer.java.versioninfo.LibraryInfoLoader; @@ -185,7 +185,8 @@ public void shouldCreateAnAuthApiClient() { assertNotNull(authClient); assertTrue( - authClient.interceptors().stream().anyMatch(i -> i.getClass().equals(IdempotencyKeyInterceptor.class)), + authClient.interceptors().stream() + .anyMatch(i -> i.getClass().equals(IdempotencyKeyGeneratorInterceptor.class)), "Idempotency interceptor not found"); assertTrue( authClient.interceptors().stream().anyMatch(i -> i.getClass().equals(TrueLayerAgentInterceptor.class)), @@ -218,10 +219,11 @@ public void shouldThrowCredentialsMissingException() { assertTrue( paymentClient.interceptors().stream() - .anyMatch(i -> i.getClass().equals(IdempotencyKeyInterceptor.class)), + .anyMatch(i -> i.getClass().equals(IdempotencyKeyGeneratorInterceptor.class)), "Idempotency interceptor not found"); assertTrue( - paymentClient.interceptors().stream().anyMatch(i -> i.getClass().equals(SignatureInterceptor.class)), + paymentClient.interceptors().stream() + .anyMatch(i -> i.getClass().equals(SignatureGeneratorInterceptor.class)), "Signature interceptor not found"); assertTrue( paymentClient.interceptors().stream() diff --git a/src/test/java/com/truelayer/java/http/interceptors/AuthenticationInterceptorTests.java b/src/test/java/com/truelayer/java/http/interceptors/AuthenticationInterceptorTests.java index 42e3ebf5..a1252f56 100644 --- a/src/test/java/com/truelayer/java/http/interceptors/AuthenticationInterceptorTests.java +++ b/src/test/java/com/truelayer/java/http/interceptors/AuthenticationInterceptorTests.java @@ -33,7 +33,7 @@ protected Interceptor getInterceptor() { @BeforeEach public void prepareTest() { - buildRequest(); + arrangeRequest(); } @Test diff --git a/src/test/java/com/truelayer/java/http/interceptors/BaseInterceptorTests.java b/src/test/java/com/truelayer/java/http/interceptors/BaseInterceptorTests.java index 4a88471a..c164cf11 100644 --- a/src/test/java/com/truelayer/java/http/interceptors/BaseInterceptorTests.java +++ b/src/test/java/com/truelayer/java/http/interceptors/BaseInterceptorTests.java @@ -14,9 +14,13 @@ public abstract class BaseInterceptorTests { protected abstract Interceptor getInterceptor(); - protected void buildRequest() { + protected void arrangeRequest() { Request request = new Request.Builder().url(HttpUrl.get("http://localhost")).build(); + arrangeRequest(request); + } + + protected void arrangeRequest(Request request) { chain = mock(Interceptor.Chain.class); when(chain.request()).thenReturn(request); } diff --git a/src/test/java/com/truelayer/java/http/interceptors/IdempotencyKeyGeneratorInterceptorTests.java b/src/test/java/com/truelayer/java/http/interceptors/IdempotencyKeyGeneratorInterceptorTests.java new file mode 100644 index 00000000..83fa4e93 --- /dev/null +++ b/src/test/java/com/truelayer/java/http/interceptors/IdempotencyKeyGeneratorInterceptorTests.java @@ -0,0 +1,48 @@ +package com.truelayer.java.http.interceptors; + +import static com.truelayer.java.Constants.HeaderNames.IDEMPOTENCY_KEY; +import static org.junit.jupiter.api.Assertions.*; + +import com.truelayer.java.TestUtils; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class IdempotencyKeyGeneratorInterceptorTests extends BaseInterceptorTests { + + @Test + @DisplayName("It should add an Idempotency-Key header") + public void shouldAddAIdempotencyKeyHeader() { + arrangeRequest(new Request.Builder() + .url("http://localhost") + .post(RequestBody.create(new byte[] {})) + .build()); + + intercept(); + + verifyThat( + request -> assertTrue(request.header(IDEMPOTENCY_KEY).matches(TestUtils.UUID_REGEX_PATTERN.pattern()))); + } + + @Test + @DisplayName("It should not add an Idempotency-Key header if already set") + public void shouldNotAddAIdempotencyKeyHeaderIfAlreadySet() { + String customIdempotencyKey = "a-ky"; + arrangeRequest(new Request.Builder() + .url("http://localhost") + .header(IDEMPOTENCY_KEY, customIdempotencyKey) + .post(RequestBody.create(new byte[] {})) + .build()); + + intercept(); + + verifyThat(request -> assertEquals(customIdempotencyKey, request.header(IDEMPOTENCY_KEY))); + } + + @Override + protected Interceptor getInterceptor() { + return new IdempotencyKeyGeneratorInterceptor(); + } +} diff --git a/src/test/java/com/truelayer/java/http/interceptors/IdempotencyKeyInterceptorTests.java b/src/test/java/com/truelayer/java/http/interceptors/IdempotencyKeyInterceptorTests.java deleted file mode 100644 index 5e1d0114..00000000 --- a/src/test/java/com/truelayer/java/http/interceptors/IdempotencyKeyInterceptorTests.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.truelayer.java.http.interceptors; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.truelayer.java.Constants; -import java.util.regex.Pattern; -import okhttp3.Interceptor; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class IdempotencyKeyInterceptorTests extends BaseInterceptorTests { - - private static final Pattern UUID_REGEX_PATTERN = - Pattern.compile("^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$"); - - @Override - protected Interceptor getInterceptor() { - return new IdempotencyKeyInterceptor(); - } - - @BeforeEach - public void prepareTest() { - buildRequest(); - } - - @Test - @DisplayName("It should add an Idempotency-Key header with a UUID to the original request") - public void shouldAddAnIdempotencyKeyHeader() { - intercept(); - - verifyThat(request -> assertTrue(UUID_REGEX_PATTERN - .matcher(request.header(Constants.HeaderNames.IDEMPOTENCY_KEY)) - .matches())); - } -} diff --git a/src/test/java/com/truelayer/java/http/interceptors/SignatureInterceptorTests.java b/src/test/java/com/truelayer/java/http/interceptors/SignatureGeneratorInterceptorTests.java similarity index 52% rename from src/test/java/com/truelayer/java/http/interceptors/SignatureInterceptorTests.java rename to src/test/java/com/truelayer/java/http/interceptors/SignatureGeneratorInterceptorTests.java index e3fdce61..e49a92fb 100644 --- a/src/test/java/com/truelayer/java/http/interceptors/SignatureInterceptorTests.java +++ b/src/test/java/com/truelayer/java/http/interceptors/SignatureGeneratorInterceptorTests.java @@ -1,9 +1,6 @@ package com.truelayer.java.http.interceptors; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; import com.truelayer.java.Constants; import com.truelayer.java.TestUtils; @@ -11,39 +8,53 @@ import java.util.UUID; import lombok.SneakyThrows; import okhttp3.Interceptor; -import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -class SignatureInterceptorTests extends BaseInterceptorTests { +class SignatureGeneratorInterceptorTests extends BaseInterceptorTests { private static final String A_PAYLOAD = "{\"foo\":\"bar\"}"; @Override protected Interceptor getInterceptor() { - return new SignatureInterceptor(TestUtils.getSigningOptions()); + return new SignatureGeneratorInterceptor(TestUtils.getSigningOptions()); } @Test @DisplayName("It should not add a Tl-Signature header on a GET request") public void shouldNotAddATlSignatureHeaderOnGet() { - prepare(new Request.Builder().url("http://localhost").get().build()); + arrangeRequest(new Request.Builder().url("http://localhost").get().build()); intercept(); verifyThat(interceptedRequest -> assertNull(interceptedRequest.header(Constants.HeaderNames.TL_SIGNATURE))); } + @Test + @DisplayName("It should not add a Tl-Signature header if already set") + public void shouldNotAddATlSignatureHeaderIfAlreadySet() { + String customSignature = "Signed!"; + arrangeRequest(new Request.Builder() + .url("http://localhost") + .header(Constants.HeaderNames.TL_SIGNATURE, customSignature) + .post(RequestBody.create(A_PAYLOAD.getBytes(StandardCharsets.UTF_8))) + .build()); + + intercept(); + + verifyThat(request -> assertEquals(customSignature, request.header(Constants.HeaderNames.TL_SIGNATURE))); + } + @Test @SneakyThrows - @DisplayName("It should add a Tl-Signature header on a GET request") + @DisplayName("It should add a Tl-Signature header on a POST request") public void shouldAddATlSignatureHeaderOnPost() { - prepare(new Request.Builder() + arrangeRequest(new Request.Builder() .url("http://localhost") .header(Constants.HeaderNames.IDEMPOTENCY_KEY, UUID.randomUUID().toString()) - .post(RequestBody.create(MediaType.get("application/json"), A_PAYLOAD.getBytes(StandardCharsets.UTF_8))) + .post(RequestBody.create(A_PAYLOAD.getBytes(StandardCharsets.UTF_8))) .build()); intercept(); @@ -51,9 +62,4 @@ public void shouldAddATlSignatureHeaderOnPost() { verifyThat(request -> assertFalse(request.header(Constants.HeaderNames.TL_SIGNATURE).isEmpty())); } - - private void prepare(Request request) { - chain = mock(Interceptor.Chain.class); - when(chain.request()).thenReturn(request); - } } diff --git a/src/test/java/com/truelayer/java/http/interceptors/TrueLayerAgentInterceptorTests.java b/src/test/java/com/truelayer/java/http/interceptors/TrueLayerAgentInterceptorTests.java index 27f9fc43..ad90a295 100644 --- a/src/test/java/com/truelayer/java/http/interceptors/TrueLayerAgentInterceptorTests.java +++ b/src/test/java/com/truelayer/java/http/interceptors/TrueLayerAgentInterceptorTests.java @@ -18,7 +18,7 @@ protected Interceptor getInterceptor() { @BeforeEach public void prepareTest() { - buildRequest(); + arrangeRequest(); } @Test diff --git a/src/test/java/com/truelayer/java/http/interceptors/logging/HttpLogMessageBuilderTests.java b/src/test/java/com/truelayer/java/http/interceptors/logging/HttpLogMessageBuilderTests.java index 7db9cbb7..3e7de475 100644 --- a/src/test/java/com/truelayer/java/http/interceptors/logging/HttpLogMessageBuilderTests.java +++ b/src/test/java/com/truelayer/java/http/interceptors/logging/HttpLogMessageBuilderTests.java @@ -3,11 +3,11 @@ import static com.truelayer.java.http.interceptors.logging.HttpLogPrefix.INCOMING; import static com.truelayer.java.http.interceptors.logging.HttpLogPrefix.OUTGOING; -import com.truelayer.java.http.entities.Header; import com.truelayer.java.http.interceptors.logging.HttpLogMessage.HttpLogMessageBuilder; import java.util.Collections; import java.util.List; import java.util.UUID; +import okhttp3.internal.http2.Header; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuardTests.java b/src/test/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuardTests.java index c76e7a1c..5d17e89c 100644 --- a/src/test/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuardTests.java +++ b/src/test/java/com/truelayer/java/http/interceptors/logging/SensitiveHeaderGuardTests.java @@ -1,11 +1,14 @@ package com.truelayer.java.http.interceptors.logging; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.truelayer.java.Constants; -import com.truelayer.java.http.entities.Header; import java.util.HashMap; import java.util.List; import java.util.Map; import okhttp3.Headers; +import okhttp3.internal.http2.Header; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,7 +16,19 @@ class SensitiveHeaderGuardTests { @Test - @DisplayName("It should mask sensitive headers") + @DisplayName("It should return an unmodifiable list") + public void shouldReturnEmptyList() { + SensitiveHeaderGuard sut = new SensitiveHeaderGuard(); + Headers headers = Headers.of(new HashMap<>()); + + List
sanitizedHeaders = sut.getSanitizedHeaders(headers); + + assertThrows(UnsupportedOperationException.class, () -> sanitizedHeaders.add(new Header("foo", "bar"))); + assertTrue(sanitizedHeaders.isEmpty()); + } + + @Test + @DisplayName("It should return an unmodifiable list with masked values") public void shouldMaskSensitiveHeaders() { SensitiveHeaderGuard sut = new SensitiveHeaderGuard(); Map headersMap = new HashMap<>(); @@ -24,9 +39,10 @@ public void shouldMaskSensitiveHeaders() { List
sanitizedHeaders = sut.getSanitizedHeaders(headers); + assertThrows(UnsupportedOperationException.class, () -> sanitizedHeaders.add(new Header("foo", "bar"))); sanitizedHeaders.forEach(h -> { - if (sut.isSensitiveHeader(h.name())) { - Assertions.assertEquals(SensitiveHeaderGuard.SENSITIVE_HEADER_MASK, h.value()); + if (sut.isSensitiveHeader(h.name.toString())) { + Assertions.assertEquals(SensitiveHeaderGuard.SENSITIVE_HEADER_MASK, h.value.toString()); } }); } diff --git a/src/test/java/com/truelayer/java/http/mappers/HeadersMapperTests.java b/src/test/java/com/truelayer/java/http/mappers/HeadersMapperTests.java new file mode 100644 index 00000000..5cf07302 --- /dev/null +++ b/src/test/java/com/truelayer/java/http/mappers/HeadersMapperTests.java @@ -0,0 +1,29 @@ +package com.truelayer.java.http.mappers; + +import static com.truelayer.java.Constants.HeaderNames.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.truelayer.java.http.entities.Headers; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HeadersMapperTests { + + @Test + @DisplayName("it should convert Headers object to an unmodifiable map") + public void shouldConvertHeadersObjectToMap() { + Headers headers = Headers.builder() + .xForwardedFor("1.2.3.4") + .signature("signed!") + .idempotencyKey("very-unique-key") + .build(); + + Map headersMap = HeadersMapper.toMap(headers); + + assertThrows(UnsupportedOperationException.class, () -> headersMap.put("foo", "bar")); + assertEquals(headers.getXForwardedFor(), headersMap.get(X_FORWARDED_FOR)); + assertEquals(headers.getSignature(), headersMap.get(TL_SIGNATURE)); + assertEquals(headers.getIdempotencyKey(), headersMap.get(IDEMPOTENCY_KEY)); + } +} diff --git a/src/test/java/com/truelayer/java/integration/HttpHeadersManagementTests.java b/src/test/java/com/truelayer/java/integration/HttpHeadersManagementTests.java new file mode 100644 index 00000000..f67c773b --- /dev/null +++ b/src/test/java/com/truelayer/java/integration/HttpHeadersManagementTests.java @@ -0,0 +1,63 @@ +package com.truelayer.java.integration; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.truelayer.java.Constants.HeaderNames.*; +import static com.truelayer.java.TestUtils.UUID_REGEX_PATTERN; + +import com.truelayer.java.TestUtils.RequestStub; +import com.truelayer.java.http.entities.Headers; +import com.truelayer.java.payments.entities.CreatePaymentRequest; +import lombok.SneakyThrows; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("HTTP Headers management integration tests") +public class HttpHeadersManagementTests extends IntegrationTests { + + @SneakyThrows + @Test + @DisplayName("Should send auto generated headers for idempotency and signature") + public void shouldSendAutoGeneratedIdempotencyKey() { + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/connect/token")) + .status(200) + .bodyFile("auth/200.access_token.json") + .build(); + CreatePaymentRequest paymentRequest = CreatePaymentRequest.builder().build(); + + tlClient.payments().createPayment(paymentRequest).get(); + + verify(postRequestedFor(urlPathEqualTo("/payments")) + .withHeader(IDEMPOTENCY_KEY, matching(UUID_REGEX_PATTERN.pattern())) + .withHeader(TL_SIGNATURE, matching(".+"))); + } + + @SneakyThrows + @Test + @DisplayName("Should send custom provided headers without triggering default auto-generation") + public void shouldSendAProvidedCustomHeadersWithoutAutoGeneration() { + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/connect/token")) + .status(200) + .bodyFile("auth/200.access_token.json") + .build(); + CreatePaymentRequest paymentRequest = CreatePaymentRequest.builder().build(); + String idempotencyKey = "custom-key"; + String signature = "custom-signature"; + + tlClient.payments() + .createPayment( + Headers.builder() + .signature(signature) + .idempotencyKey(idempotencyKey) + .build(), + paymentRequest) + .get(); + + verify(postRequestedFor(urlPathEqualTo("/payments")) + .withHeader(TL_SIGNATURE, equalTo(signature)) + .withHeader(IDEMPOTENCY_KEY, equalTo(idempotencyKey))); + } +} diff --git a/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java b/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java index 26263175..8c93e350 100644 --- a/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java +++ b/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java @@ -10,6 +10,7 @@ import com.truelayer.java.TestUtils; import com.truelayer.java.TestUtils.RequestStub; import com.truelayer.java.http.entities.ApiResponse; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.http.entities.ProblemDetails; import com.truelayer.java.payments.entities.*; import com.truelayer.java.payments.entities.paymentdetail.PaymentDetail; @@ -252,7 +253,8 @@ public void shouldStartAnAuthorizationFlowWithXForwardedForHeader() { StartAuthorizationFlowRequest.builder().build(); ApiResponse response = tlClient.payments() - .startAuthorizationFlow(A_PAYMENT_ID, request, endUserIpAddress) + .startAuthorizationFlow( + Headers.builder().xForwardedFor(endUserIpAddress).build(), A_PAYMENT_ID, request) .get(); assertNotError(response); diff --git a/src/test/java/com/truelayer/java/mandates/MandatesHandlerTests.java b/src/test/java/com/truelayer/java/mandates/MandatesHandlerTests.java index 49e72fdc..3a549f65 100644 --- a/src/test/java/com/truelayer/java/mandates/MandatesHandlerTests.java +++ b/src/test/java/com/truelayer/java/mandates/MandatesHandlerTests.java @@ -1,8 +1,11 @@ package com.truelayer.java.mandates; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static com.truelayer.java.TestUtils.buildTestHeaders; +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; +import static org.mockito.Mockito.*; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.mandates.entities.CreateMandateRequest; import com.truelayer.java.mandates.entities.ListMandatesQuery; import com.truelayer.java.payments.entities.StartAuthorizationFlowRequest; @@ -20,7 +23,7 @@ class MandatesHandlerTests { private static final String A_CURRENCY = "GBP"; @Test - @DisplayName("It should call the create mandate endpoint") + @DisplayName("It should call the create mandate endpoint with empty headers map") public void shouldCallCreateMandate() { IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); MandatesHandler sut = new MandatesHandler(mandatesApi); @@ -28,11 +31,25 @@ public void shouldCallCreateMandate() { sut.createMandate(request); - verify(mandatesApi, times(1)).createMandate(request); + verify(mandatesApi, times(1)).createMandate(emptyMap(), request); } @Test - @DisplayName("It should call the start authorization flow endpoint") + @DisplayName("It should call the create mandate endpoint with custom headers") + public void shouldCallCreateMandateWithCustomHeaders() { + IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); + MandatesHandler sut = new MandatesHandler(mandatesApi); + CreateMandateRequest request = CreateMandateRequest.builder().build(); + Headers customHeaders = buildTestHeaders(); + + sut.createMandate(customHeaders, request); + + verify(mandatesApi, times(1)).createMandate(toMap(customHeaders), request); + verifyNoMoreInteractions(mandatesApi); + } + + @Test + @DisplayName("It should call the start authorization flow endpoint with empty headers map") public void shouldCallStartAuthFlow() { IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); MandatesHandler sut = new MandatesHandler(mandatesApi); @@ -41,11 +58,25 @@ public void shouldCallStartAuthFlow() { sut.startAuthorizationFlow(A_MANDATE_ID, request); - verify(mandatesApi, times(1)).startAuthorizationFlow(A_MANDATE_ID, request); + verify(mandatesApi, times(1)).startAuthorizationFlow(emptyMap(), A_MANDATE_ID, request); + } + + @Test + @DisplayName("It should call the start authorization flow endpoint with custom headers") + public void shouldCallStartAuthFlowWithCustomHeaders() { + IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); + MandatesHandler sut = new MandatesHandler(mandatesApi); + Headers customHeaders = buildTestHeaders(); + StartAuthorizationFlowRequest request = + StartAuthorizationFlowRequest.builder().build(); + + sut.startAuthorizationFlow(customHeaders, A_MANDATE_ID, request); + + verify(mandatesApi, times(1)).startAuthorizationFlow(toMap(customHeaders), A_MANDATE_ID, request); } @Test - @DisplayName("It should call the submit provider endpoint") + @DisplayName("It should call the submit provider endpoint with empty headers map") public void shouldCallSubmitProviderEndpoint() { IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); MandatesHandler sut = new MandatesHandler(mandatesApi); @@ -54,7 +85,21 @@ public void shouldCallSubmitProviderEndpoint() { sut.submitProviderSelection(A_MANDATE_ID, request); - verify(mandatesApi, times(1)).submitProviderSelection(A_MANDATE_ID, request); + verify(mandatesApi, times(1)).submitProviderSelection(emptyMap(), A_MANDATE_ID, request); + } + + @Test + @DisplayName("It should call the submit provider endpoint with custom headers") + public void shouldCallSubmitProviderEndpointWithCustomHeaders() { + IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); + MandatesHandler sut = new MandatesHandler(mandatesApi); + Headers customHeaders = buildTestHeaders(); + SubmitProviderSelectionRequest request = + SubmitProviderSelectionRequest.builder().build(); + + sut.submitProviderSelection(customHeaders, A_MANDATE_ID, request); + + verify(mandatesApi, times(1)).submitProviderSelection(toMap(customHeaders), A_MANDATE_ID, request); } @Test @@ -96,14 +141,26 @@ public void shouldCallGetMandatesEndpoint() { } @Test - @DisplayName("It should call the revoke mandate endpoint") + @DisplayName("It should call the revoke mandate endpoint with empty headers map") public void shouldCallRevokeMandatesEndpoint() { IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); MandatesHandler sut = new MandatesHandler(mandatesApi); sut.revokeMandate(A_MANDATE_ID); - verify(mandatesApi, times(1)).revokeMandate(A_MANDATE_ID); + verify(mandatesApi, times(1)).revokeMandate(emptyMap(), A_MANDATE_ID); + } + + @Test + @DisplayName("It should call the revoke mandate endpoint with custom headers") + public void shouldCallRevokeMandatesEndpointWithCustomHeaders() { + IMandatesApi mandatesApi = Mockito.mock(IMandatesApi.class); + MandatesHandler sut = new MandatesHandler(mandatesApi); + Headers customHeaders = buildTestHeaders(); + + sut.revokeMandate(customHeaders, A_MANDATE_ID); + + verify(mandatesApi, times(1)).revokeMandate(toMap(customHeaders), A_MANDATE_ID); } @Test diff --git a/src/test/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandlerTests.java b/src/test/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandlerTests.java index f207a4ed..a0e01bf8 100644 --- a/src/test/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandlerTests.java +++ b/src/test/java/com/truelayer/java/merchantaccounts/MerchantAccountsHandlerTests.java @@ -1,9 +1,13 @@ package com.truelayer.java.merchantaccounts; +import static com.truelayer.java.TestUtils.buildTestHeaders; +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import com.truelayer.java.entities.CurrencyCode; +import com.truelayer.java.http.entities.Headers; import com.truelayer.java.merchantaccounts.entities.ListPaymentSourcesQuery; import com.truelayer.java.merchantaccounts.entities.ListTransactionsQuery; import com.truelayer.java.merchantaccounts.entities.UpdateSweepingRequest; @@ -63,7 +67,7 @@ public void shouldCallListTransactionsEndpoint() { } @Test - @DisplayName("It should call the update sweeping endpoint") + @DisplayName("It should call the update sweeping endpoint with empty headers map") public void shouldCallUpdateSweepingEndpoint() { IMerchantAccountsApi merchantsApi = Mockito.mock(IMerchantAccountsApi.class); MerchantAccountsHandler sut = new MerchantAccountsHandler(merchantsApi); @@ -72,7 +76,21 @@ public void shouldCallUpdateSweepingEndpoint() { sut.updateSweeping(A_MERCHANT_ACCOUNT_ID, request); - verify(merchantsApi, times(1)).updateSweeping(A_MERCHANT_ACCOUNT_ID, request); + verify(merchantsApi, times(1)).updateSweeping(emptyMap(), A_MERCHANT_ACCOUNT_ID, request); + } + + @Test + @DisplayName("It should call the update sweeping endpoint with custom headers") + public void shouldCallUpdateSweepingEndpointWithCustomHeaders() { + IMerchantAccountsApi merchantsApi = Mockito.mock(IMerchantAccountsApi.class); + MerchantAccountsHandler sut = new MerchantAccountsHandler(merchantsApi); + Headers customHeaders = buildTestHeaders(); + UpdateSweepingRequest request = + UpdateSweepingRequest.builder().currency(CurrencyCode.GBP).build(); + + sut.updateSweeping(customHeaders, A_MERCHANT_ACCOUNT_ID, request); + + verify(merchantsApi, times(1)).updateSweeping(toMap(customHeaders), A_MERCHANT_ACCOUNT_ID, request); } @Test @@ -87,14 +105,26 @@ public void shouldCallGetSweepingSettingsEndpoint() { } @Test - @DisplayName("It should call the disable sweeping endpoint") + @DisplayName("It should call the disable sweeping endpoint with empty headers map") public void shouldCallDisableSweepingEndpoint() { IMerchantAccountsApi merchantsApi = Mockito.mock(IMerchantAccountsApi.class); MerchantAccountsHandler sut = new MerchantAccountsHandler(merchantsApi); sut.disableSweeping(A_MERCHANT_ACCOUNT_ID); - verify(merchantsApi, times(1)).disableSweeping(A_MERCHANT_ACCOUNT_ID); + verify(merchantsApi, times(1)).disableSweeping(emptyMap(), A_MERCHANT_ACCOUNT_ID); + } + + @Test + @DisplayName("It should call the disable sweeping endpoint") + public void shouldCallDisableSweepingEndpointWithCustomHeaders() { + IMerchantAccountsApi merchantsApi = Mockito.mock(IMerchantAccountsApi.class); + MerchantAccountsHandler sut = new MerchantAccountsHandler(merchantsApi); + Headers customHeaders = buildTestHeaders(); + + sut.disableSweeping(customHeaders, A_MERCHANT_ACCOUNT_ID); + + verify(merchantsApi, times(1)).disableSweeping(toMap(customHeaders), A_MERCHANT_ACCOUNT_ID); } @Test diff --git a/src/test/java/com/truelayer/java/payments/PaymentsHandlerTests.java b/src/test/java/com/truelayer/java/payments/PaymentsHandlerTests.java new file mode 100644 index 00000000..2d746b2f --- /dev/null +++ b/src/test/java/com/truelayer/java/payments/PaymentsHandlerTests.java @@ -0,0 +1,213 @@ +package com.truelayer.java.payments; + +import static com.truelayer.java.TestUtils.buildTestHeaders; +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.truelayer.java.TestUtils; +import com.truelayer.java.entities.EmptyRequestBody; +import com.truelayer.java.http.entities.Headers; +import com.truelayer.java.payments.entities.*; +import java.util.Collections; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class PaymentsHandlerTests { + + private static final String A_PAYMENT_ID = "a-payment-id"; + private static final String A_REFUND_ID = "a-refund-id"; + + @Test + @DisplayName("It should call the create payment endpoint") + public void shouldCallCreatePayment() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + CreatePaymentRequest request = CreatePaymentRequest.builder().build(); + + sut.createPayment(request); + + verify(paymentsApi, times(1)).createPayment(emptyMap(), request); + } + + @Test + @DisplayName("It should call the create payment endpoint with additional headers") + public void shouldCallCreatePaymentWithAdditionalHeaders() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + Headers customHeaders = buildTestHeaders(); + CreatePaymentRequest request = CreatePaymentRequest.builder().build(); + + sut.createPayment(customHeaders, request); + + verify(paymentsApi, times(1)).createPayment(toMap(customHeaders), request); + } + + @Test + @DisplayName("It should call the get payment by id endpoint") + public void shouldCallGetPaymentById() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + + sut.getPayment(A_PAYMENT_ID); + + verify(paymentsApi, times(1)).getPayment(A_PAYMENT_ID); + } + + @Test + @DisplayName("It should call the start authorization flow endpoint") + public void shouldCallStartAuthorizationFlow() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + StartAuthorizationFlowRequest request = + StartAuthorizationFlowRequest.builder().withProviderSelection().build(); + + sut.startAuthorizationFlow(A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).startAuthorizationFlow(emptyMap(), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the start authorization flow endpoint with additional headers") + public void shouldCallStartAuthorizationFlowWithCustomHeaders() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + Headers customHeaders = buildTestHeaders(); + StartAuthorizationFlowRequest request = + StartAuthorizationFlowRequest.builder().withProviderSelection().build(); + + sut.startAuthorizationFlow(customHeaders, A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).startAuthorizationFlow(toMap(customHeaders), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the submit provider selection endpoint") + public void shouldCallSubmitProviderSelection() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + SubmitProviderSelectionRequest request = SubmitProviderSelectionRequest.builder() + .providerId("a-provider-id") + .build(); + + sut.submitProviderSelection(A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).submitProviderSelection(emptyMap(), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the submit provider selection endpoint with additional headers") + public void shouldCallSubmitProviderSelectionWithCustomHeaders() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + Headers customHeaders = TestUtils.buildTestHeaders(); + SubmitProviderSelectionRequest request = SubmitProviderSelectionRequest.builder() + .providerId("a-provider-id") + .build(); + + sut.submitProviderSelection(customHeaders, A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).submitProviderSelection(toMap(customHeaders), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the submit consent endpoint") + public void shouldCallSubmitConsent() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + + sut.submitConsent(A_PAYMENT_ID); + + verify(paymentsApi, times(1)).submitConsent(emptyMap(), A_PAYMENT_ID, new EmptyRequestBody()); + } + + @Test + @DisplayName("It should call the submit consent endpoint with additional headers") + public void shouldCallSubmitConsentWithCustomHeaders() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + Headers customHeaders = TestUtils.buildTestHeaders(); + + sut.submitConsent(customHeaders, A_PAYMENT_ID); + + verify(paymentsApi, times(1)).submitConsent(toMap(customHeaders), A_PAYMENT_ID, new EmptyRequestBody()); + } + + @Test + @DisplayName("It should call the submit form endpoint") + public void shouldCallSubmitForm() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + SubmitFormRequest request = + SubmitFormRequest.builder().inputs(Collections.emptyMap()).build(); + + sut.submitForm(A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).submitForm(emptyMap(), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the submit form endpoint with additional headers") + public void shouldCallSubmitFormWithCustomHeaders() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + Headers customHeaders = TestUtils.buildTestHeaders(); + SubmitFormRequest request = + SubmitFormRequest.builder().inputs(Collections.emptyMap()).build(); + + sut.submitForm(customHeaders, A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).submitForm(toMap(customHeaders), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the create payment refund endpoint") + public void shouldCallCreatePaymentRefund() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + CreatePaymentRefundRequest request = + CreatePaymentRefundRequest.builder().amountInMinor(100).build(); + + sut.createPaymentRefund(A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).createPaymentRefund(emptyMap(), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the create payment refund endpoint with additional headers") + public void shouldCallCreatePaymentRefundWithCustomHeaders() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + Headers customHeaders = TestUtils.buildTestHeaders(); + CreatePaymentRefundRequest request = + CreatePaymentRefundRequest.builder().amountInMinor(100).build(); + + sut.createPaymentRefund(customHeaders, A_PAYMENT_ID, request); + + verify(paymentsApi, times(1)).createPaymentRefund(toMap(customHeaders), A_PAYMENT_ID, request); + } + + @Test + @DisplayName("It should call the list payment refunds endpoint") + public void shouldCallListPaymentRefunds() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + + sut.listPaymentRefunds(A_PAYMENT_ID); + + verify(paymentsApi, times(1)).listPaymentRefunds(A_PAYMENT_ID); + } + + @Test + @DisplayName("It should call the get payment refund by id endpoint") + public void shouldCallGetPaymentRefundById() { + IPaymentsApi paymentsApi = Mockito.mock(IPaymentsApi.class); + PaymentsHandler sut = new PaymentsHandler(paymentsApi); + + sut.getPaymentRefundById(A_PAYMENT_ID, A_REFUND_ID); + + verify(paymentsApi, times(1)).getPaymentRefundById(A_PAYMENT_ID, A_REFUND_ID); + } +} diff --git a/src/test/java/com/truelayer/java/payouts/PayoutsHandlerTests.java b/src/test/java/com/truelayer/java/payouts/PayoutsHandlerTests.java new file mode 100644 index 00000000..b677c362 --- /dev/null +++ b/src/test/java/com/truelayer/java/payouts/PayoutsHandlerTests.java @@ -0,0 +1,57 @@ +package com.truelayer.java.payouts; + +import static com.truelayer.java.http.mappers.HeadersMapper.toMap; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.truelayer.java.TestUtils; +import com.truelayer.java.http.entities.Headers; +import com.truelayer.java.payouts.entities.CreatePayoutRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class PayoutsHandlerTests { + + private static String A_PAYOUT_ID = "a-payout-id"; + + @Test + @DisplayName("It should call the create payout endpoint with empty headers map") + public void shouldCallCreatePayoutEndpoint() { + IPayoutsApi payoutsApi = Mockito.mock(IPayoutsApi.class); + PayoutsHandler sut = new PayoutsHandler(payoutsApi); + CreatePayoutRequest request = + CreatePayoutRequest.builder().amountInMinor(100).build(); + + sut.createPayout(request); + + verify(payoutsApi, times(1)).createPayout(emptyMap(), request); + } + + @Test + @DisplayName("It should call the create payout endpoint with custom headers") + public void shouldCallCreatePayoutEndpointWithCustomHeaders() { + IPayoutsApi payoutsApi = Mockito.mock(IPayoutsApi.class); + PayoutsHandler sut = new PayoutsHandler(payoutsApi); + Headers customHeaders = TestUtils.buildTestHeaders(); + CreatePayoutRequest request = + CreatePayoutRequest.builder().amountInMinor(100).build(); + + sut.createPayout(customHeaders, request); + + verify(payoutsApi, times(1)).createPayout(toMap(customHeaders), request); + } + + @Test + @DisplayName("It should call the get payout by id endpoint") + public void shouldCallGetPayoutByIdEndpoint() { + IPayoutsApi payoutsApi = Mockito.mock(IPayoutsApi.class); + PayoutsHandler sut = new PayoutsHandler(payoutsApi); + + sut.getPayout(A_PAYOUT_ID); + + verify(payoutsApi, times(1)).getPayout(A_PAYOUT_ID); + } +}