diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index f862c838..9f2f2360 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -89,8 +89,8 @@ jobs: "text": "I don't like that", "emoji": true }, - "image_url": "https://media.giphy.com/media/4cuyucPeVWbNS/giphy.gif", - "alt_text": "marg" + "image_url": "https://media.giphy.com/media/jOpLbiGmHR9S0/giphy.gif", + "alt_text": "this is the worst" } ] } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 51884ba1..5e1a2915 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Main properties group=com.truelayer archivesBaseName=truelayer-java -version=10.3.1 +version=11.0.0 # Artifacts properties sonatype_repository_url=https://s01.oss.sonatype.org/service/local/ diff --git a/src/main/java/com/truelayer/java/Constants.java b/src/main/java/com/truelayer/java/Constants.java index 60c3efb1..af0df87d 100644 --- a/src/main/java/com/truelayer/java/Constants.java +++ b/src/main/java/com/truelayer/java/Constants.java @@ -12,6 +12,8 @@ public static final class Scopes { public static final String PAYMENTS = "payments"; public static final String RECURRING_PAYMENTS_SWEEPING = "recurring_payments:sweeping"; + + public static final String RECURRING_PAYMENTS_COMMERCIAL = "recurring_payments:commercial"; } /** diff --git a/src/main/java/com/truelayer/java/ITrueLayerClient.java b/src/main/java/com/truelayer/java/ITrueLayerClient.java index dd0d5db5..ff056f9b 100644 --- a/src/main/java/com/truelayer/java/ITrueLayerClient.java +++ b/src/main/java/com/truelayer/java/ITrueLayerClient.java @@ -1,8 +1,8 @@ package com.truelayer.java; import com.truelayer.java.auth.IAuthenticationHandler; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse; import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.mandates.IMandatesHandler; @@ -65,6 +65,6 @@ public interface ITrueLayerClient { * @return the response of the Submit payment returns parameters operation * @see Submit payments return parameters API reference */ - CompletableFuture> submitPaymentReturnParameters( - SubmitPaymentReturnParametersRequest request); + CompletableFuture> submitPaymentReturnParameters( + SubmitPaymentsProviderReturnRequest request); } diff --git a/src/main/java/com/truelayer/java/TrueLayerClient.java b/src/main/java/com/truelayer/java/TrueLayerClient.java index e1c45b89..7e95b28d 100644 --- a/src/main/java/com/truelayer/java/TrueLayerClient.java +++ b/src/main/java/com/truelayer/java/TrueLayerClient.java @@ -2,8 +2,8 @@ import com.truelayer.java.auth.IAuthenticationHandler; import com.truelayer.java.commonapi.ICommonHandler; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse; import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.mandates.IMandatesHandler; @@ -120,8 +120,8 @@ public IHostedPaymentPageLinkBuilder hpp() { * {@inheritDoc} */ @Override - public CompletableFuture> submitPaymentReturnParameters( - SubmitPaymentReturnParametersRequest request) { + public CompletableFuture> submitPaymentReturnParameters( + SubmitPaymentsProviderReturnRequest request) { return commonHandler.submitPaymentReturnParameters(request); } diff --git a/src/main/java/com/truelayer/java/commonapi/CommonHandler.java b/src/main/java/com/truelayer/java/commonapi/CommonHandler.java index ea47a669..de16877a 100644 --- a/src/main/java/com/truelayer/java/commonapi/CommonHandler.java +++ b/src/main/java/com/truelayer/java/commonapi/CommonHandler.java @@ -3,8 +3,8 @@ 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.commonapi.entities.SubmitPaymentsProviderReturnRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.http.entities.Headers; import java.util.concurrent.CompletableFuture; @@ -16,14 +16,14 @@ public class CommonHandler implements ICommonHandler { ICommonApi commonApi; @Override - public CompletableFuture> submitPaymentReturnParameters( - SubmitPaymentReturnParametersRequest request) { + public CompletableFuture> submitPaymentReturnParameters( + SubmitPaymentsProviderReturnRequest request) { return commonApi.submitPaymentReturnParameters(emptyMap(), request); } @Override - public CompletableFuture> submitPaymentReturnParameters( - Headers headers, SubmitPaymentReturnParametersRequest request) { + public CompletableFuture> submitPaymentReturnParameters( + Headers headers, SubmitPaymentsProviderReturnRequest 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 e07486b3..c283ce39 100644 --- a/src/main/java/com/truelayer/java/commonapi/ICommonApi.java +++ b/src/main/java/com/truelayer/java/commonapi/ICommonApi.java @@ -1,7 +1,7 @@ package com.truelayer.java.commonapi; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse; import com.truelayer.java.http.entities.ApiResponse; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -24,6 +24,6 @@ public interface ICommonApi { * @see Submit payments return parameters API reference */ @POST("/payments-provider-return") - CompletableFuture> submitPaymentReturnParameters( - @HeaderMap Map headers, @Body SubmitPaymentReturnParametersRequest request); + CompletableFuture> submitPaymentReturnParameters( + @HeaderMap Map headers, @Body SubmitPaymentsProviderReturnRequest request); } diff --git a/src/main/java/com/truelayer/java/commonapi/ICommonHandler.java b/src/main/java/com/truelayer/java/commonapi/ICommonHandler.java index 7ad0611c..fcc71c70 100644 --- a/src/main/java/com/truelayer/java/commonapi/ICommonHandler.java +++ b/src/main/java/com/truelayer/java/commonapi/ICommonHandler.java @@ -1,16 +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.commonapi.entities.SubmitPaymentsProviderReturnRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse; 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( + SubmitPaymentsProviderReturnRequest request); - CompletableFuture> submitPaymentReturnParameters( - Headers headers, SubmitPaymentReturnParametersRequest request); + CompletableFuture> submitPaymentReturnParameters( + Headers headers, SubmitPaymentsProviderReturnRequest request); } diff --git a/src/main/java/com/truelayer/java/commonapi/entities/MandateProviderReturnResource.java b/src/main/java/com/truelayer/java/commonapi/entities/MandateProviderReturnResource.java new file mode 100644 index 00000000..b1d8272d --- /dev/null +++ b/src/main/java/com/truelayer/java/commonapi/entities/MandateProviderReturnResource.java @@ -0,0 +1,12 @@ +package com.truelayer.java.commonapi.entities; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class MandateProviderReturnResource extends ProviderReturnResource { + Type type = Type.MANDATE; + + String mandateId; +} diff --git a/src/main/java/com/truelayer/java/commonapi/entities/PaymentProviderReturnResource.java b/src/main/java/com/truelayer/java/commonapi/entities/PaymentProviderReturnResource.java new file mode 100644 index 00000000..b845433c --- /dev/null +++ b/src/main/java/com/truelayer/java/commonapi/entities/PaymentProviderReturnResource.java @@ -0,0 +1,12 @@ +package com.truelayer.java.commonapi.entities; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class PaymentProviderReturnResource extends ProviderReturnResource { + Type type = Type.PAYMENT; + + String paymentId; +} diff --git a/src/main/java/com/truelayer/java/commonapi/entities/ProviderReturnResource.java b/src/main/java/com/truelayer/java/commonapi/entities/ProviderReturnResource.java new file mode 100644 index 00000000..e43dde86 --- /dev/null +++ b/src/main/java/com/truelayer/java/commonapi/entities/ProviderReturnResource.java @@ -0,0 +1,65 @@ +package com.truelayer.java.commonapi.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonValue; +import com.truelayer.java.TrueLayerException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = PaymentProviderReturnResource.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = PaymentProviderReturnResource.class, name = "payment"), + @JsonSubTypes.Type(value = MandateProviderReturnResource.class, name = "mandate") +}) +@Getter +public abstract class ProviderReturnResource { + String type; + + String paymentId; + + @JsonIgnore + public abstract Type getType(); + + @JsonIgnore + public boolean isPaymentProviderReturnResponse() { + return this instanceof PaymentProviderReturnResource; + } + + @JsonIgnore + public boolean isMandateProviderReturnResponse() { + return this instanceof MandateProviderReturnResource; + } + + @JsonIgnore + public PaymentProviderReturnResource asPaymentProviderReturnResponse() { + if (!isPaymentProviderReturnResponse()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (PaymentProviderReturnResource) this; + } + + @JsonIgnore + public MandateProviderReturnResource asMandateProviderReturnResponse() { + if (!isMandateProviderReturnResponse()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (MandateProviderReturnResource) this; + } + + private String buildErrorMessage() { + return String.format( + "Provider return resource is of type %s.", this.getClass().getSimpleName()); + } + + @RequiredArgsConstructor + @Getter + public enum Type { + PAYMENT("payment"), + MANDATE("mandate"); + + @JsonValue + private final String type; + } +} diff --git a/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentReturnParametersResponse.java b/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentReturnParametersResponse.java deleted file mode 100644 index 145d12eb..00000000 --- a/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentReturnParametersResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.truelayer.java.commonapi.entities; - -import lombok.Value; - -@Value -public class SubmitPaymentReturnParametersResponse { - - Resource resource; - - @Value - public static class Resource { - String type; - - String paymentId; - } -} diff --git a/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentReturnParametersRequest.java b/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentsProviderReturnRequest.java similarity index 83% rename from src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentReturnParametersRequest.java rename to src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentsProviderReturnRequest.java index 77ec145a..7377175a 100644 --- a/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentReturnParametersRequest.java +++ b/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentsProviderReturnRequest.java @@ -9,7 +9,7 @@ @Getter @ToString @EqualsAndHashCode -public class SubmitPaymentReturnParametersRequest { +public class SubmitPaymentsProviderReturnRequest { private String query; private String fragment; diff --git a/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentsProviderReturnResponse.java b/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentsProviderReturnResponse.java new file mode 100644 index 00000000..6cd8b275 --- /dev/null +++ b/src/main/java/com/truelayer/java/commonapi/entities/SubmitPaymentsProviderReturnResponse.java @@ -0,0 +1,8 @@ +package com.truelayer.java.commonapi.entities; + +import lombok.Value; + +@Value +public class SubmitPaymentsProviderReturnResponse { + ProviderReturnResource resource; +} diff --git a/src/test/java/com/truelayer/java/TestUtils.java b/src/test/java/com/truelayer/java/TestUtils.java index 4fb88025..d40f92b2 100644 --- a/src/test/java/com/truelayer/java/TestUtils.java +++ b/src/test/java/com/truelayer/java/TestUtils.java @@ -4,13 +4,16 @@ import static com.truelayer.java.Constants.HeaderNames.*; import static com.truelayer.java.Utils.getObjectMapper; import static org.apache.commons.lang3.ObjectUtils.*; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.*; import com.github.tomakehurst.wiremock.client.MappingBuilder; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; import com.github.tomakehurst.wiremock.matching.UrlPattern; import com.github.tomakehurst.wiremock.stubbing.StubMapping; import com.truelayer.java.auth.entities.AccessToken; +import com.truelayer.java.commonapi.entities.PaymentProviderReturnResource; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.http.entities.Headers; import com.truelayer.java.versioninfo.VersionInfo; @@ -20,8 +23,10 @@ import java.time.Duration; import java.util.UUID; import java.util.regex.Pattern; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import okhttp3.OkHttpClient; +import okhttp3.*; import org.apache.commons.lang3.RandomStringUtils; public class TestUtils { @@ -217,4 +222,75 @@ public static Headers buildTestHeaders() { .xDeviceUserAgent("ADummyUserAgen") .build(); } + + @SneakyThrows + public static void runAndAssertHeadlessResourceAuthorisation( + TrueLayerClient tlClient, URI redirectUri, HeadlessResourceAuthorization headlessResourceAuthorization) { + assertNotNull(redirectUri); + String protocol = redirectUri.getScheme(); + String host = redirectUri.getHost(); + String resourceId = + redirectUri.getPath().substring(redirectUri.getPath().lastIndexOf("/") + 1); + var body = RequestBody.create(MediaType.get("application/json"), headlessResourceAuthorization.payload); + Request request = new Request.Builder() + .url(String.format( + "%s://%s/api/%s/%s/action", protocol, host, headlessResourceAuthorization.path, resourceId)) + .post(body) + .addHeader( + "Authorization", + String.format("Bearer %s", redirectUri.getFragment().replaceFirst("token=", ""))) + .build(); + Response authorisationResponse = + getHttpClientInstance().newCall(request).execute(); + assertTrue(authorisationResponse.isSuccessful()); + String responseString = authorisationResponse.body().string(); + assertNotNull(responseString); + + // Grab the provider return query and fragment from the mock payment api response + URI responseUrl = URI.create(responseString); + + // Mandates require some adjustments to the query string... + // TODO: review if we want this to be a permanent solution + String query = headlessResourceAuthorization == HeadlessResourceAuthorization.MANDATES + ? responseUrl.getQuery().replaceFirst("mandate-", "") + : responseUrl.getQuery(); + + SubmitPaymentsProviderReturnRequest submitProviderReturneRequest = SubmitPaymentsProviderReturnRequest.builder() + .query(query) + .fragment(responseUrl.getFragment()) + .build(); + ApiResponse submitPaymentReturnParametersResponse = + tlClient.submitPaymentReturnParameters(submitProviderReturneRequest) + .get(); + assertNotError(submitPaymentReturnParametersResponse); + + switch (headlessResourceAuthorization) { + case PAYMENTS: + assertEquals( + PaymentProviderReturnResource.Type.PAYMENT, + submitPaymentReturnParametersResponse + .getData() + .getResource() + .getType()); + break; + case MANDATES: + assertEquals( + PaymentProviderReturnResource.Type.MANDATE, + submitPaymentReturnParametersResponse + .getData() + .getResource() + .getType()); + break; + } + } + + @RequiredArgsConstructor + @Getter + public enum HeadlessResourceAuthorization { + PAYMENTS("single-immediate-payments", "{\"action\":\"Execute\", \"redirect\": false}"), + MANDATES("vrp-consents", "{\"action\":\"Authorise\", \"redirect\": false}"); + + private final String path; + private final String payload; + } } diff --git a/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java index 5791804d..d727dc14 100644 --- a/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java @@ -1,5 +1,7 @@ package com.truelayer.java.acceptance; +import static com.truelayer.java.Constants.Scopes.*; +import static com.truelayer.java.TestUtils.*; import static com.truelayer.java.TestUtils.assertNotError; import static com.truelayer.java.TestUtils.getHttpClientInstance; import static com.truelayer.java.mandates.entities.Constraints.PeriodicLimits.Limit.PeriodAlignment.CALENDAR; @@ -8,10 +10,8 @@ import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.*; -import com.truelayer.java.entities.CurrencyCode; -import com.truelayer.java.entities.ProviderFilter; -import com.truelayer.java.entities.RelatedProducts; -import com.truelayer.java.entities.User; +import com.truelayer.java.*; +import com.truelayer.java.entities.*; import com.truelayer.java.entities.accountidentifier.AccountIdentifier; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.mandates.entities.*; @@ -26,6 +26,7 @@ import com.truelayer.java.payments.entities.providerselection.ProviderSelection; import com.truelayer.java.payments.entities.retry.Retry; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Objects; import java.util.UUID; @@ -37,7 +38,6 @@ import okhttp3.RequestBody; import okhttp3.Response; import org.apache.commons.lang3.ObjectUtils; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -49,17 +49,21 @@ public class MandatesAcceptanceTests extends AcceptanceTests { public static final String RETURN_URI = "http://localhost:3000/callback"; - public static final String PROVIDER_ID = "ob-uki-mock-bank-sbox"; + public static final String PROVIDER_ID = "mock-payments-gb-redirect"; - @Test - @DisplayName("It should create a VRP sweeping mandate with preselected provider") + @ParameterizedTest + @DisplayName("It should create a VRP mandate with preselected provider") + @MethodSource("provideMandatesScopesAndTypes") @SneakyThrows - public void itShouldCreateASweepingMandateWithPreselectedProvider() { + public void itShouldCreateAMandateWithPreselectedProvider(String mandatesScope, Mandate.Type mandateType) { + // create client with required scopes + var tlClient = buildMandatesTlClient(mandatesScope); + // create mandate ProviderSelection preselectedProvider = ProviderSelection.preselected().providerId(PROVIDER_ID).build(); ApiResponse createMandateResponse = tlClient.mandates() - .createMandate(createMandateRequest(SWEEPING, preselectedProvider)) + .createMandate(createMandateRequest(mandateType, preselectedProvider)) .get(); assertNotError(createMandateResponse); @@ -85,25 +89,6 @@ public void itShouldCreateASweepingMandateWithSignupPlusIntention() { assertNotError(createMandateResponse); } - @Test - @Disabled("The provider ob-uki-mock-bank-sbox does not support commercial VRP yet") - @DisplayName("It should create a VRP commercial mandate with preselected provider") - @SneakyThrows - public void itShouldCreateACommercialMandateWithPreselectedProvider() { - // create mandate - ProviderSelection preselectedProvider = - ProviderSelection.preselected().providerId(PROVIDER_ID).build(); - ApiResponse createMandateResponse = tlClient.mandates() - .createMandate(createMandateRequest(COMMERCIAL, preselectedProvider)) - .get(); - assertNotError(createMandateResponse); - - // start auth flow - ApiResponse startAuthorizationFlowResponse = - startAuthFlowForMandate(createMandateResponse.getData().getId()); - assertNotError(startAuthorizationFlowResponse); - } - @Test @DisplayName("It should create a mandate with user_selected provider") @SneakyThrows @@ -143,15 +128,19 @@ public void itShouldGetAListOfMandates() { assertNotError(listMandatesResponse); } - @Test + @ParameterizedTest @DisplayName("It should get confirm funds") + @MethodSource("provideMandatesScopesAndTypes") @SneakyThrows - public void itShouldGetFunds() { + public void itShouldGetFunds(String mandatesScope, Mandate.Type mandateType) { + // create client with required scopes + var tlClient = buildMandatesTlClient(mandatesScope); + // create mandate ProviderSelection preselectedProvider = ProviderSelection.preselected().providerId(PROVIDER_ID).build(); ApiResponse createMandateResponse = tlClient.mandates() - .createMandate(createMandateRequest(SWEEPING, preselectedProvider)) + .createMandate(createMandateRequest(mandateType, preselectedProvider)) .get(); assertNotError(createMandateResponse); @@ -169,8 +158,22 @@ public void itShouldGetFunds() { assertNotError(startAuthorizationFlowResponse); + // we check the state of the mandate returned by the gateway + AuthorizationFlowResponse authorizationFlowResponse = startAuthorizationFlowResponse.getData(); + assertTrue( + authorizationFlowResponse.isAuthorizing(), + "Mandate status is " + authorizationFlowResponse.getStatus().getStatus()); + // authorize the created mandate without explicit user interaction - authorizeMandate(startAuthorizationFlowResponse.getData()); + URI redirectUri = authorizationFlowResponse + .asAuthorizing() + .getAuthorizationFlow() + .getActions() + .getNext() + .asRedirect() + .getUri(); + runAndAssertHeadlessResourceAuthorisation(tlClient, redirectUri, HeadlessResourceAuthorization.MANDATES); + waitForMandateToBeAuthorized(createMandateResponse.getData().getId()); // finally make a confirmation of funds request for 1 penny @@ -182,14 +185,18 @@ public void itShouldGetFunds() { assertEquals(true, getConfirmationOfFundsResponseApiResponse.getData().getConfirmed()); } - @Test + @ParameterizedTest @DisplayName("It should get mandate constraints") + @MethodSource("provideMandatesScopesAndTypes") @SneakyThrows - public void itShouldGetConstraints() { + public void itShouldGetConstraints(String mandatesScope, Mandate.Type mandateType) { + // create client with required scopes + var tlClient = buildMandatesTlClient(mandatesScope); + // create mandate ProviderSelection preselectedProvider = ProviderSelection.preselected().providerId(PROVIDER_ID).build(); - CreateMandateRequest createMandateRequest = createMandateRequest(SWEEPING, preselectedProvider); + CreateMandateRequest createMandateRequest = createMandateRequest(mandateType, preselectedProvider); ApiResponse createMandateResponse = tlClient.mandates().createMandate(createMandateRequest).get(); assertNotError(createMandateResponse); @@ -208,8 +215,22 @@ public void itShouldGetConstraints() { assertNotError(startAuthorizationFlowResponse); + // we check the state of the mandate returned by the gateway + AuthorizationFlowResponse authorizationFlowResponse = startAuthorizationFlowResponse.getData(); + assertTrue( + authorizationFlowResponse.isAuthorizing(), + "Mandate status is " + authorizationFlowResponse.getStatus().getStatus()); + // authorize the created mandate without explicit user interaction - authorizeMandate(startAuthorizationFlowResponse.getData()); + URI redirectUri = authorizationFlowResponse + .asAuthorizing() + .getAuthorizationFlow() + .getActions() + .getNext() + .asRedirect() + .getUri(); + runAndAssertHeadlessResourceAuthorisation(tlClient, redirectUri, HeadlessResourceAuthorization.MANDATES); + waitForMandateToBeAuthorized(createMandateResponse.getData().getId()); // finally make a Get constraints request @@ -220,15 +241,19 @@ public void itShouldGetConstraints() { assertNotError(getConstraintsResponseApiResponse); } - @Test + @ParameterizedTest @DisplayName("It should create and revoke a mandate") + @MethodSource("provideMandatesScopesAndTypes") @SneakyThrows - public void itShouldCreateAndRevokeAMandate() { + public void itShouldCreateAndRevokeAMandate(String mandatesScope, Mandate.Type mandateType) { + // create client with required scopes + var tlClient = buildMandatesTlClient(mandatesScope); + // create mandate ProviderSelection preselectedProvider = ProviderSelection.preselected().providerId(PROVIDER_ID).build(); ApiResponse createMandateResponse = tlClient.mandates() - .createMandate(createMandateRequest(SWEEPING, preselectedProvider)) + .createMandate(createMandateRequest(mandateType, preselectedProvider)) .get(); assertNotError(createMandateResponse); @@ -237,8 +262,22 @@ public void itShouldCreateAndRevokeAMandate() { startAuthFlowForMandate(createMandateResponse.getData().getId()); assertNotError(startAuthorizationFlowResponse); + // we check the state of the mandate returned by the gateway + AuthorizationFlowResponse authorizationFlowResponse = startAuthorizationFlowResponse.getData(); + assertTrue( + authorizationFlowResponse.isAuthorizing(), + "Mandate status is " + authorizationFlowResponse.getStatus().getStatus()); + // authorize the created mandate without explicit user interaction - authorizeMandate(startAuthorizationFlowResponse.getData()); + URI redirectUri = authorizationFlowResponse + .asAuthorizing() + .getAuthorizationFlow() + .getActions() + .getNext() + .asRedirect() + .getUri(); + runAndAssertHeadlessResourceAuthorisation(tlClient, redirectUri, HeadlessResourceAuthorization.MANDATES); + waitForMandateToBeAuthorized(createMandateResponse.getData().getId()); // revoke mandate by id @@ -252,12 +291,15 @@ public void itShouldCreateAndRevokeAMandate() { @ParameterizedTest(name = "with retry {0}") @MethodSource("provideItShouldCreateAPaymentOnMandateTestParameters") @SneakyThrows - public void itShouldCreateAPaymentOnMandate(Retry retry) { + public void itShouldCreateAPaymentOnMandate(String mandatesScope, Mandate.Type mandateType, Retry retry) { + // create client with required scopes + var tlClient = buildMandatesTlClient(mandatesScope); + // create mandate ProviderSelection preselectedProvider = ProviderSelection.preselected().providerId(PROVIDER_ID).build(); ApiResponse createMandateResponse = tlClient.mandates() - .createMandate(createMandateRequest(SWEEPING, preselectedProvider)) + .createMandate(createMandateRequest(mandateType, preselectedProvider)) .get(); assertNotError(createMandateResponse); @@ -275,8 +317,22 @@ public void itShouldCreateAPaymentOnMandate(Retry retry) { assertNotError(startAuthorizationFlowResponse); + // we check the state of the mandate returned by the gateway + AuthorizationFlowResponse authorizationFlowResponse = startAuthorizationFlowResponse.getData(); + assertTrue( + authorizationFlowResponse.isAuthorizing(), + "Mandate status is " + authorizationFlowResponse.getStatus().getStatus()); + // authorize the created mandate without explicit user interaction - authorizeMandate(startAuthorizationFlowResponse.getData()); + URI redirectUri = authorizationFlowResponse + .asAuthorizing() + .getAuthorizationFlow() + .getActions() + .getNext() + .asRedirect() + .getUri(); + runAndAssertHeadlessResourceAuthorisation(tlClient, redirectUri, HeadlessResourceAuthorization.MANDATES); + waitForMandateToBeAuthorized(createMandateResponse.getData().getId()); // get mandate by id @@ -381,6 +437,7 @@ private ApiResponse startAuthFlowForMandate(String ma } @SneakyThrows + @Deprecated // TODO: cleanup private void authorizeMandate(AuthorizationFlowResponse authorizationFlowResponse) { // first we check the state of the mandate returned by the gateway assertTrue( @@ -395,6 +452,7 @@ private void authorizeMandate(AuthorizationFlowResponse authorizationFlowRespons .getNext() .asRedirect() .getUri(); + Response redirectResponse = getHttpClientInstance() .newCall(new Request.Builder().url(redirectUri.toURL()).get().build()) .execute(); @@ -438,13 +496,57 @@ private void waitForMandateToBeAuthorized(String mandateId) { }); } - public static Stream provideItShouldCreateAPaymentOnMandateTestParameters() { + private static TrueLayerClient buildMandatesTlClient(String mandatesScope) { + return TrueLayerClient.New() + .environment(environment) + .clientCredentials(ClientCredentials.builder() + .clientId(System.getenv("TL_CLIENT_ID")) + .clientSecret(System.getenv("TL_CLIENT_SECRET")) + .build()) + .signingOptions(SigningOptions.builder() + .keyId(System.getenv("TL_SIGNING_KEY_ID")) + .privateKey(System.getenv("TL_SIGNING_PRIVATE_KEY").getBytes(StandardCharsets.UTF_8)) + .build()) + .withGlobalScopes(RequestScopes.builder() + .scope(PAYMENTS) + .scope(mandatesScope) + .build()) + .withHttpLogs() + .withCredentialsCaching() + .build(); + } + + private static Stream provideItShouldCreateAPaymentOnMandateTestParameters() { + return Stream.of( + Arguments.of(RECURRING_PAYMENTS_SWEEPING, SWEEPING, null), + Arguments.of(RECURRING_PAYMENTS_COMMERCIAL, COMMERCIAL, null), + Arguments.of( + RECURRING_PAYMENTS_SWEEPING, + SWEEPING, + Retry.standard().forDuration("30m").build()), + Arguments.of( + RECURRING_PAYMENTS_COMMERCIAL, + COMMERCIAL, + Retry.standard().forDuration("30m").build()), + Arguments.of( + RECURRING_PAYMENTS_SWEEPING, + SWEEPING, + Retry.smart() + .forDuration("90d") + .ensureMinimumBalanceInMinor(100) + .build()), + Arguments.of( + RECURRING_PAYMENTS_COMMERCIAL, + COMMERCIAL, + Retry.smart() + .forDuration("90d") + .ensureMinimumBalanceInMinor(100) + .build())); + } + + private static Stream provideMandatesScopesAndTypes() { return Stream.of( - null, - Arguments.of(Retry.standard().forDuration("30m").build()), - Arguments.of(Retry.smart() - .forDuration("90d") - .ensureMinimumBalanceInMinor(100) - .build())); + Arguments.of(RECURRING_PAYMENTS_SWEEPING, SWEEPING), + Arguments.of(RECURRING_PAYMENTS_COMMERCIAL, COMMERCIAL)); } } diff --git a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java index 58acc393..6fbbc829 100644 --- a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java @@ -4,8 +4,6 @@ import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.*; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; import com.truelayer.java.entities.*; import com.truelayer.java.entities.Address; import com.truelayer.java.entities.accountidentifier.AccountIdentifier; @@ -348,21 +346,15 @@ public void shouldCompleteAnAuthorizationFlowForAPaymentWithProviderReturn() { .get(); assertNotError(startAuthorizationFlowResponse); - // Call the mock payment api response to trigger the executed state on the payment just created - Response paymentResponse = callMockProviderRedirectUrl(startAuthorizationFlowResponse.getData()); - assertTrue(paymentResponse.isSuccessful()); - String responseString = paymentResponse.body().string(); - assertNotNull(responseString); - - // Grab the provider return query and fragment from the mock payment api response - URI responseUrl = URI.create(responseString); - SubmitPaymentReturnParametersRequest submitProviderReturn = SubmitPaymentReturnParametersRequest.builder() - .query(responseUrl.getQuery()) - .fragment(responseUrl.getFragment()) - .build(); - ApiResponse submitPaymentReturnParametersResponse = - tlClient.submitPaymentReturnParameters(submitProviderReturn).get(); - assertNotError(submitPaymentReturnParametersResponse); + URI redirectUri = startAuthorizationFlowResponse + .getData() + .asAuthorizing() + .getAuthorizationFlow() + .getActions() + .getNext() + .asRedirect() + .getUri(); + runAndAssertHeadlessResourceAuthorisation(tlClient, redirectUri, HeadlessResourceAuthorization.PAYMENTS); } @SneakyThrows @@ -394,21 +386,15 @@ public void shouldCreateAPaymentRefundAndGetRefundDetails() { .get(); assertNotError(startAuthorizationFlowResponse); - // Call the mock payment api response to trigger the executed state on the payment just created - Response paymentResponse = callMockProviderRedirectUrl(startAuthorizationFlowResponse.getData()); - assertTrue(paymentResponse.isSuccessful()); - String responseString = paymentResponse.body().string(); - assertNotNull(responseString); - - // Grab the provider return query and fragment from the mock payment api response - URI responseUrl = URI.create(responseString); - SubmitPaymentReturnParametersRequest submitProviderReturn = SubmitPaymentReturnParametersRequest.builder() - .query(responseUrl.getQuery()) - .fragment(responseUrl.getFragment()) - .build(); - ApiResponse submitPaymentReturnParametersResponse = - tlClient.submitPaymentReturnParameters(submitProviderReturn).get(); - assertNotError(submitPaymentReturnParametersResponse); + URI redirectUri = startAuthorizationFlowResponse + .getData() + .asAuthorizing() + .getAuthorizationFlow() + .getActions() + .getNext() + .asRedirect() + .getUri(); + runAndAssertHeadlessResourceAuthorisation(tlClient, redirectUri, HeadlessResourceAuthorization.PAYMENTS); waitForPaymentToBeSettled(paymentId); @@ -455,31 +441,6 @@ public void shouldCreateAPaymentRefundAndGetRefundDetails() { paymentRefundApiResponse.getData().getMetadata()); } - @SneakyThrows - private Response callMockProviderRedirectUrl(AuthorizationFlowResponse startAuthorizationFlowResponse) { - URI redirectUri = startAuthorizationFlowResponse - .asAuthorizing() - .getAuthorizationFlow() - .getActions() - .getNext() - .asRedirect() - .getUri(); - assertNotNull(redirectUri); - String protocol = redirectUri.getScheme(); - String host = redirectUri.getHost(); - String bankPaymentId = redirectUri.getPath().replaceFirst("/login/", ""); - RequestBody body = - RequestBody.create(MediaType.get("application/json"), "{\"action\":\"Execute\", \"redirect\": false}"); - Request request = new Request.Builder() - .url(String.format("%s://%s/api/single-immediate-payments/%s/action", protocol, host, bankPaymentId)) - .post(body) - .addHeader( - "Authorization", - String.format("Bearer %s", redirectUri.getFragment().replaceFirst("token=", ""))) - .build(); - return getHttpClientInstance().newCall(request).execute(); - } - private PreselectedProviderSelection buildPreselectedProviderSelection() { return ProviderSelection.preselected() .providerId(PROVIDER_ID) diff --git a/src/test/java/com/truelayer/java/acceptance/PaymentsProvidersAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/PaymentsProvidersAcceptanceTests.java index 0b81863a..ed3df47c 100644 --- a/src/test/java/com/truelayer/java/acceptance/PaymentsProvidersAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/PaymentsProvidersAcceptanceTests.java @@ -6,7 +6,6 @@ import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.paymentsproviders.entities.PaymentsProvider; import lombok.SneakyThrows; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -17,8 +16,6 @@ public class PaymentsProvidersAcceptanceTests extends AcceptanceTests { public static final String PROVIDER_ID = "mock-payments-gb-redirect"; @Test - @Disabled( - "Temporarily disabled because the mock is released in alpha, but the release channel enum deliberately doesn't have alpha") @DisplayName("It should get by id a payments provider") @SneakyThrows public void shouldCreateAPaymentWithUserSelectionProvider() { diff --git a/src/test/java/com/truelayer/java/commonapi/CommonHandlerTests.java b/src/test/java/com/truelayer/java/commonapi/CommonHandlerTests.java index 0b448488..e876cbbd 100644 --- a/src/test/java/com/truelayer/java/commonapi/CommonHandlerTests.java +++ b/src/test/java/com/truelayer/java/commonapi/CommonHandlerTests.java @@ -6,7 +6,7 @@ import static org.mockito.Mockito.verify; import com.truelayer.java.TestUtils; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest; import com.truelayer.java.http.entities.Headers; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -19,7 +19,7 @@ class CommonHandlerTests { public void shouldCallSubmitPaymentReturnParamsEndpoint() { ICommonApi commonApi = Mockito.mock(ICommonApi.class); CommonHandler sut = new CommonHandler(commonApi); - SubmitPaymentReturnParametersRequest request = SubmitPaymentReturnParametersRequest.builder() + SubmitPaymentsProviderReturnRequest request = SubmitPaymentsProviderReturnRequest.builder() .query("a-query") .fragment("a-fragment") .build(); @@ -35,7 +35,7 @@ public void shouldCallSubmitPaymentReturnParamsEndpointWithCustomHeaders() { ICommonApi commonApi = Mockito.mock(ICommonApi.class); CommonHandler sut = new CommonHandler(commonApi); Headers customHeaders = TestUtils.buildTestHeaders(); - SubmitPaymentReturnParametersRequest request = SubmitPaymentReturnParametersRequest.builder() + SubmitPaymentsProviderReturnRequest request = SubmitPaymentsProviderReturnRequest.builder() .query("a-query") .fragment("a-fragment") .build(); diff --git a/src/test/java/com/truelayer/java/commonapi/entities/ProviderReturnResourceTests.java b/src/test/java/com/truelayer/java/commonapi/entities/ProviderReturnResourceTests.java new file mode 100644 index 00000000..5f7a01d1 --- /dev/null +++ b/src/test/java/com/truelayer/java/commonapi/entities/ProviderReturnResourceTests.java @@ -0,0 +1,77 @@ +package com.truelayer.java.commonapi.entities; + +import static org.junit.jupiter.api.Assertions.*; + +import com.truelayer.java.TrueLayerException; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ProviderReturnResourceTests { + + @Test + @DisplayName("It should yield true if instance is of type PaymentProviderReturnResource") + public void shouldYieldTrueIfPaymentProviderReturnResource() { + ProviderReturnResource sut = + new PaymentProviderReturnResource(UUID.randomUUID().toString()); + + assertTrue(sut.isPaymentProviderReturnResponse()); + } + + @Test + @DisplayName("It should convert to an instance of class PaymentProviderReturnResource") + public void shouldConvertToPaymentProviderReturnResource() { + ProviderReturnResource sut = + new PaymentProviderReturnResource(UUID.randomUUID().toString()); + + assertDoesNotThrow(sut::asPaymentProviderReturnResponse); + } + + @Test + @DisplayName("It should throw an error when converting to PaymentProviderReturnResource") + public void shouldNotConvertToPaymentProviderReturnResource() { + ProviderReturnResource sut = + new MandateProviderReturnResource(UUID.randomUUID().toString()); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asPaymentProviderReturnResponse); + + assertEquals( + String.format( + "Provider return resource is of type %s.", + sut.getClass().getSimpleName()), + thrown.getMessage()); + } + + @Test + @DisplayName("It should yield true if instance is of type MandateProviderReturnResource") + public void shouldYieldTrueIfMandateProviderReturnResource() { + ProviderReturnResource sut = + new MandateProviderReturnResource(UUID.randomUUID().toString()); + + assertTrue(sut.isMandateProviderReturnResponse()); + } + + @Test + @DisplayName("It should convert to an instance of class MandateProviderReturnResource") + public void shouldConvertToMandateProviderReturnResource() { + ProviderReturnResource sut = + new MandateProviderReturnResource(UUID.randomUUID().toString()); + + assertDoesNotThrow(sut::asMandateProviderReturnResponse); + } + + @Test + @DisplayName("It should throw an error when converting to MandateProviderReturnResource") + public void shouldNotConvertToMandateProviderReturnResource() { + ProviderReturnResource sut = + new PaymentProviderReturnResource(UUID.randomUUID().toString()); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asMandateProviderReturnResponse); + + assertEquals( + String.format( + "Provider return resource is of type %s.", + sut.getClass().getSimpleName()), + thrown.getMessage()); + } +} diff --git a/src/test/java/com/truelayer/java/integration/CommonApiIntegrationTests.java b/src/test/java/com/truelayer/java/integration/CommonApiIntegrationTests.java index 17abb494..b95e4865 100644 --- a/src/test/java/com/truelayer/java/integration/CommonApiIntegrationTests.java +++ b/src/test/java/com/truelayer/java/integration/CommonApiIntegrationTests.java @@ -6,8 +6,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.truelayer.java.TestUtils.RequestStub; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersRequest; -import com.truelayer.java.commonapi.entities.SubmitPaymentReturnParametersResponse; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest; +import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse; import com.truelayer.java.http.entities.ApiResponse; import lombok.SneakyThrows; import org.junit.jupiter.api.DisplayName; @@ -28,15 +28,15 @@ public void shouldReturnAResourceWhenProviderReturnParamsSubmitted() { .bodyFile(jsonResponseFile) .build(); - ApiResponse submitPaymentReturnParametersResponse = + ApiResponse submitPaymentReturnParametersResponse = tlClient.submitPaymentReturnParameters( - SubmitPaymentReturnParametersRequest.builder().build()) + SubmitPaymentsProviderReturnRequest.builder().build()) .get(); verify(exactly(0), postRequestedFor(urlPathEqualTo("/connect/token"))); assertNotError(submitPaymentReturnParametersResponse); - SubmitPaymentReturnParametersResponse expected = - deserializeJsonFileTo(jsonResponseFile, SubmitPaymentReturnParametersResponse.class); + SubmitPaymentsProviderReturnResponse expected = + deserializeJsonFileTo(jsonResponseFile, SubmitPaymentsProviderReturnResponse.class); assertEquals(expected, submitPaymentReturnParametersResponse.getData()); } }