diff --git a/yoti-sdk-impl/findbugs-rules.xml b/yoti-sdk-impl/findbugs-rules.xml index d7d8a334f..485262fd3 100644 --- a/yoti-sdk-impl/findbugs-rules.xml +++ b/yoti-sdk-impl/findbugs-rules.xml @@ -10,6 +10,16 @@ + + + + + + + + + + diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/HttpMethod.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/HttpMethod.java index 039d996f2..1a3d9489b 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/HttpMethod.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/HttpMethod.java @@ -1,6 +1,18 @@ package com.yoti.api.client.spi.remote.call; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import java.util.List; + public class HttpMethod { + public static final String HTTP_GET = "GET"; public static final String HTTP_POST = "POST"; + public static final String HTTP_PUT = "PUT"; + public static final String HTTP_PATCH = "PATCH"; + public static final String HTTP_DELETE = "DELETE"; + + public static final List SUPPORTED_HTTP_METHODS = unmodifiableList(asList(HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_GET, HTTP_DELETE)); + } diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/JsonResourceFetcher.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/JsonResourceFetcher.java index 1d71688e1..4993bac69 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/JsonResourceFetcher.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/JsonResourceFetcher.java @@ -32,15 +32,17 @@ public static JsonResourceFetcher newInstance() { @Override public T fetchResource(UrlConnector urlConnector, Map headers, Class resourceClass) throws ResourceException, IOException { - HttpURLConnection httpUrlConnection = openConnection(urlConnector, HTTP_GET, headers); - return parseResponse(httpUrlConnection, resourceClass); + return doRequest(urlConnector, HTTP_GET, null, headers, resourceClass); } @Override public T postResource(UrlConnector urlConnector, byte[] body, Map headers, Class resourceClass) throws ResourceException, IOException { + return doRequest(urlConnector, HTTP_POST, body, headers, resourceClass); + } - HttpURLConnection httpUrlConnection = openConnection(urlConnector, HTTP_POST, headers); + public T doRequest(UrlConnector urlConnector, String httpMethod, byte[] body, Map headers, Class resourceClass) throws ResourceException, IOException { + HttpURLConnection httpUrlConnection = openConnection(urlConnector, httpMethod, headers); sendBody(body, httpUrlConnection); return parseResponse(httpUrlConnection, resourceClass); } diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/RemoteProfileService.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/RemoteProfileService.java index d03dd6233..ca69b5156 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/RemoteProfileService.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/RemoteProfileService.java @@ -5,21 +5,21 @@ import static com.yoti.api.client.spi.remote.Base64.base64; import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_GET; +import static com.yoti.api.client.spi.remote.call.YotiConstants.AUTH_KEY_HEADER; import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_YOTI_API_URL; import static com.yoti.api.client.spi.remote.call.YotiConstants.PROPERTY_YOTI_API_URL; import static com.yoti.api.client.spi.remote.util.Validation.notNull; import java.io.IOException; +import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.Security; -import java.util.Map; import com.yoti.api.client.ProfileException; -import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; -import com.yoti.api.client.spi.remote.call.factory.PathFactory; -import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; +import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,32 +27,26 @@ public final class RemoteProfileService implements ProfileService { private static final Logger LOG = LoggerFactory.getLogger(RemoteProfileService.class); - private final ResourceFetcher resourceFetcher; - private final PathFactory pathFactory; - private final HeadersFactory headersFactory; - private final SignedMessageFactory signedMessageFactory; + private final UnsignedPathFactory unsignedPathFactory; + private final SignedRequestBuilder signedRequestBuilder; private final String apiUrl; static { - Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + Security.addProvider(new BouncyCastleProvider()); } public static RemoteProfileService newInstance() { return new RemoteProfileService( - JsonResourceFetcher.newInstance(), - new PathFactory(), - new HeadersFactory(), - SignedMessageFactory.newInstance()); + new UnsignedPathFactory(), + SignedRequestBuilder.newInstance() + ); } - RemoteProfileService(ResourceFetcher resourceFetcher, - PathFactory profilePathFactory, - HeadersFactory headersFactory, - SignedMessageFactory signedMessageFactory) { - this.resourceFetcher = resourceFetcher; - this.pathFactory = profilePathFactory; - this.headersFactory = headersFactory; - this.signedMessageFactory = signedMessageFactory; + RemoteProfileService(UnsignedPathFactory profilePathFactory, + SignedRequestBuilder signedRequestBuilder) { + this.unsignedPathFactory = profilePathFactory; + this.signedRequestBuilder = signedRequestBuilder; + apiUrl = System.getProperty(PROPERTY_YOTI_API_URL, DEFAULT_YOTI_API_URL); } @@ -62,26 +56,32 @@ public Receipt getReceipt(KeyPair keyPair, String appId, String connectToken) th notNull(appId, "Application id"); notNull(connectToken, "Connect token"); - String path = pathFactory.createProfilePath(appId, connectToken); + String path = unsignedPathFactory.createProfilePath(appId, connectToken); try { - String digest = signedMessageFactory.create(keyPair.getPrivate(), HTTP_GET, path); String authKey = base64(keyPair.getPublic().getEncoded()); - return fetchReceipt(path, digest, authKey); + + SignedRequest signedRequest = this.signedRequestBuilder + .withKeyPair(keyPair) + .withBaseUrl(apiUrl) + .withEndpoint(path) + .withHttpMethod(HTTP_GET) + .withHeader(AUTH_KEY_HEADER, authKey) + .build(); + return fetchReceipt(signedRequest); } catch (GeneralSecurityException gse) { throw new ProfileException("Cannot sign request", gse); + } catch (URISyntaxException uriSyntaxException) { + throw new ProfileException("Error creating request", uriSyntaxException); } catch (IOException ioe) { throw new ProfileException("Error calling service to get profile", ioe); } } - private Receipt fetchReceipt(String resourcePath, String digest, String authKey) throws IOException, ProfileException { - LOG.info("Fetching profile from resource at '{}'", resourcePath); - Map headers = headersFactory.create(digest, authKey); - UrlConnector urlConnector = UrlConnector.get(apiUrl + resourcePath); - + private Receipt fetchReceipt(SignedRequest signedRequest) throws IOException, ProfileException { + LOG.info("Fetching profile from resource at '{}'", signedRequest.getUri()); try { - ProfileResponse response = resourceFetcher.fetchResource(urlConnector, headers, ProfileResponse.class); + ProfileResponse response = signedRequest.execute(ProfileResponse.class); return response.getReceipt(); } catch (ResourceException re) { int responseCode = re.getResponseCode(); diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequest.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequest.java new file mode 100644 index 000000000..ef96e2aed --- /dev/null +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequest.java @@ -0,0 +1,48 @@ +package com.yoti.api.client.spi.remote.call; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +public class SignedRequest { + + private final URI uri; + private final String method; + private final byte[] data; + private final Map headers; + private final JsonResourceFetcher jsonResourceFetcher; + + SignedRequest(final URI uri, + final String method, + final byte[] data, + final Map headers, + JsonResourceFetcher jsonResourceFetcher) { + + this.uri = uri; + this.method = method; + this.data = data; + this.headers = headers; + this.jsonResourceFetcher = jsonResourceFetcher; + } + + public URI getUri() { + return uri; + } + + public String getMethod() { + return method; + } + + public byte[] getData() { + return data != null ? data.clone() : null; + } + + public Map getHeaders() { + return headers; + } + + public T execute(Class clazz) throws ResourceException, IOException { + UrlConnector urlConnector = UrlConnector.get(uri.toString()); + return jsonResourceFetcher.doRequest(urlConnector, getMethod(), getData(), getHeaders(), clazz); + } +} diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java new file mode 100644 index 000000000..fbf0f4293 --- /dev/null +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java @@ -0,0 +1,188 @@ +package com.yoti.api.client.spi.remote.call; + +import static com.yoti.api.client.spi.remote.call.HttpMethod.SUPPORTED_HTTP_METHODS; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.HashMap; +import java.util.Map; + +import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; +import com.yoti.api.client.spi.remote.call.factory.PathFactory; +import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; +import com.yoti.api.client.spi.remote.util.Validation; + +public class SignedRequestBuilder { + + public static SignedRequestBuilder newInstance() { + return new SignedRequestBuilder( + new PathFactory(), + SignedMessageFactory.newInstance(), + new HeadersFactory(), + JsonResourceFetcher.newInstance() + ); + } + + private KeyPair keyPair; + private String baseUrl; + private String endpoint; + private byte[] payload; + private final Map queryParameters = new HashMap<>(); + private final Map headers = new HashMap<>(); + private String httpMethod; + + private final PathFactory pathFactory; + private final SignedMessageFactory signedMessageFactory; + private final HeadersFactory headersFactory; + private final JsonResourceFetcher jsonResourceFetcher; + + SignedRequestBuilder(PathFactory pathFactory, + SignedMessageFactory signedMessageFactory, + HeadersFactory headersFactory, + JsonResourceFetcher jsonResourceFetcher) { + this.pathFactory = pathFactory; + this.signedMessageFactory = signedMessageFactory; + this.headersFactory = headersFactory; + this.jsonResourceFetcher = jsonResourceFetcher; + } + + /** + * Proceed building the signed request with a specific key pair + * + * @param keyPair the key pair + * @return the updated builder + */ + public SignedRequestBuilder withKeyPair(KeyPair keyPair) { + this.keyPair = keyPair; + return this; + } + + /** + * Proceed building the signed request with specific base url + * + * @param baseUrl the base url + * @return the updated builder + */ + public SignedRequestBuilder withBaseUrl(String baseUrl) { + this.baseUrl = baseUrl.replaceAll("([/]+)$", ""); + return this; + } + + /** + * Proceed building the signed request with a specific endpoint + * + * @param endpoint the endpoint + * @return the updated builder + */ + public SignedRequestBuilder withEndpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + /** + * Proceed building the signed request with a specific payload + * + * @param payload the payload + * @return the update builder + */ + public SignedRequestBuilder withPayload(byte[] payload) { + this.payload = payload; + return this; + } + + /** + * Proceed building the signed request with a specific query parameter + * + * @param name the name of the query parameter + * @param value the value of the query parameter + * @return the updated builder + */ + public SignedRequestBuilder withQueryParameter(String name, String value) { + this.queryParameters.put(name, value); + return this; + } + + /** + * Proceed building the signed request with a specific header + * + * @param name the name of the header + * @param value the value of the header + * @return the updated builder + */ + public SignedRequestBuilder withHeader(String name, String value) { + this.headers.put(name, value); + return this; + } + + /** + * Proceed building the signed request with a specific HTTP method + * + * @param httpMethod the HTTP method + * @return the updated builder + */ + public SignedRequestBuilder withHttpMethod(String httpMethod) throws IllegalArgumentException { + Validation.withinList(httpMethod, SUPPORTED_HTTP_METHODS); + this.httpMethod = httpMethod; + return this; + } + + /** + * Build the signed request with specified options + * + * @return the signed request + */ + public SignedRequest build() throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { + validateRequest(); + + if (endpoint.contains("?")) { + endpoint = endpoint.concat("&"); + } else { + endpoint = endpoint.concat("?"); + } + + String builtEndpoint = endpoint + createQueryParameterString(queryParameters); + String digest = createDigest(builtEndpoint); + headers.putAll(headersFactory.create(digest)); + + return new SignedRequest( + new URI(baseUrl + builtEndpoint), + httpMethod, + payload, + headers, + jsonResourceFetcher + ); + } + + private void validateRequest() { + Validation.notNull(keyPair, "keyPair"); + Validation.notNullOrEmpty(baseUrl, "baseUrl"); + Validation.notNullOrEmpty(endpoint, "endpoint"); + Validation.notNullOrEmpty(httpMethod, "httpMethod"); + } + + private String createQueryParameterString(Map queryParameters) throws UnsupportedEncodingException { + StringBuilder stringBuilder = new StringBuilder(); + for (Map.Entry entry : queryParameters.entrySet()) { + stringBuilder.append(entry.getKey()); + stringBuilder.append("="); + stringBuilder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.toString())); + stringBuilder.append("&"); + } + stringBuilder.append(pathFactory.createSignatureParams()); + return stringBuilder.toString(); + } + + private String createDigest(String endpoint) throws GeneralSecurityException { + if (payload != null) { + return signedMessageFactory.create(keyPair.getPrivate(), httpMethod, endpoint, payload); + } else { + return signedMessageFactory.create(keyPair.getPrivate(), httpMethod, endpoint); + } + } + +} diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlService.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlService.java index 986ae586d..9c64794b3 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlService.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlService.java @@ -1,60 +1,44 @@ package com.yoti.api.client.spi.remote.call.aml; -import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; -import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; -import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; - -import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_POST; -import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_CHARSET; -import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_YOTI_API_URL; -import static com.yoti.api.client.spi.remote.call.YotiConstants.PROPERTY_YOTI_API_URL; -import static com.yoti.api.client.spi.remote.util.Validation.notNull; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yoti.api.client.AmlException; +import com.yoti.api.client.aml.AmlProfile; +import com.yoti.api.client.spi.remote.call.ResourceException; +import com.yoti.api.client.spi.remote.call.SignedRequest; +import com.yoti.api.client.spi.remote.call.SignedRequestBuilder; +import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import java.io.IOException; +import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.security.KeyPair; -import java.util.Map; - -import com.yoti.api.client.AmlException; -import com.yoti.api.client.aml.AmlProfile; -import com.yoti.api.client.spi.remote.call.JsonResourceFetcher; -import com.yoti.api.client.spi.remote.call.ResourceException; -import com.yoti.api.client.spi.remote.call.ResourceFetcher; -import com.yoti.api.client.spi.remote.call.UrlConnector; -import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; -import com.yoti.api.client.spi.remote.call.factory.PathFactory; -import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; -import com.fasterxml.jackson.databind.ObjectMapper; +import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_POST; +import static com.yoti.api.client.spi.remote.call.YotiConstants.*; +import static com.yoti.api.client.spi.remote.util.Validation.notNull; +import static java.net.HttpURLConnection.*; public class RemoteAmlService { - private final PathFactory pathFactory; - private final HeadersFactory headersFactory; + private final UnsignedPathFactory unsignedPathFactory; private final ObjectMapper objectMapper; - private final SignedMessageFactory signedMessageFactory; - private final ResourceFetcher resourceFetcher; + private final SignedRequestBuilder signedRequestBuilder; private final String apiUrl; public static RemoteAmlService newInstance() { return new RemoteAmlService( - JsonResourceFetcher.newInstance(), - new PathFactory(), - new HeadersFactory(), + new UnsignedPathFactory(), new ObjectMapper(), - SignedMessageFactory.newInstance()); + SignedRequestBuilder.newInstance() + ); } - public RemoteAmlService(ResourceFetcher resourceFetcher, - PathFactory pathFactory, - HeadersFactory headersFactory, - ObjectMapper objectMapper, - SignedMessageFactory signedMessageFactory) { - this.pathFactory = pathFactory; - this.headersFactory = headersFactory; + RemoteAmlService(UnsignedPathFactory unsignedPathFactory, + ObjectMapper objectMapper, + SignedRequestBuilder signedRequestBuilder) { + this.unsignedPathFactory = unsignedPathFactory; this.objectMapper = objectMapper; - this.signedMessageFactory = signedMessageFactory; - this.resourceFetcher = resourceFetcher; + this.signedRequestBuilder = signedRequestBuilder; apiUrl = System.getProperty(PROPERTY_YOTI_API_URL, DEFAULT_YOTI_API_URL); } @@ -65,16 +49,23 @@ public SimpleAmlResult performCheck(KeyPair keyPair, String appId, AmlProfile am notNull(amlProfile, "amlProfile"); try { - String resourcePath = pathFactory.createAmlPath(appId); + String resourcePath = unsignedPathFactory.createAmlPath(appId); byte[] body = objectMapper.writeValueAsString(amlProfile).getBytes(DEFAULT_CHARSET); - String digest = signedMessageFactory.create(keyPair.getPrivate(), HTTP_POST, resourcePath, body); - Map headers = headersFactory.create(digest); - UrlConnector urlConnector = UrlConnector.get(apiUrl + resourcePath); - return resourceFetcher.postResource(urlConnector, body, headers, SimpleAmlResult.class); + + SignedRequest signedRequest = this.signedRequestBuilder.withKeyPair(keyPair) + .withBaseUrl(apiUrl) + .withEndpoint(resourcePath) + .withPayload(body) + .withHttpMethod(HTTP_POST) + .build(); + + return signedRequest.execute(SimpleAmlResult.class); } catch (IOException ioException) { throw new AmlException("Error communicating with AML endpoint", ioException); } catch (GeneralSecurityException generalSecurityException) { throw new AmlException("Cannot sign request", generalSecurityException); + } catch (URISyntaxException uriSyntaxException) { + throw new AmlException("Error creating request", uriSyntaxException); } catch (ResourceException resourceException) { throw createExceptionFromStatusCode(resourceException); } diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/HeadersFactory.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/HeadersFactory.java index 005fb6d28..c6f2fa486 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/HeadersFactory.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/HeadersFactory.java @@ -23,6 +23,7 @@ public Map create(String digest) { return headers; } + @Deprecated public Map create(String digest, String authKey) { Map headers = create(digest); headers.put(AUTH_KEY_HEADER, authKey); diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/PathFactory.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/PathFactory.java index 73e94e83c..2c6cbef42 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/PathFactory.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/PathFactory.java @@ -1,25 +1,32 @@ package com.yoti.api.client.spi.remote.call.factory; -import static java.lang.String.format; import static java.lang.System.nanoTime; import static java.util.UUID.randomUUID; public class PathFactory { - private static final String PROFILE_PATH_TEMPLATE = "/profile/%s?nonce=%s×tamp=%s&appId=%s"; - private static final String AML_PATH_TEMPLATE = "/aml-check?appId=%s&nonce=%s×tamp=%s"; - private static final String QR_CODE_PATH_TEMPLATE = "/qrcodes/apps/%s?nonce=%s×tamp=%s"; + private static final String SIGNATURE_PARAMS = "nonce=%s×tamp=%s"; + + private UnsignedPathFactory unsignedPathFactory; + + public PathFactory() { + this.unsignedPathFactory = new UnsignedPathFactory(); + } + + public String createSignatureParams() { + return String.format(SIGNATURE_PARAMS, randomUUID(), createTimestamp()); + } public String createProfilePath(String appId, String connectToken) { - return format(PROFILE_PATH_TEMPLATE, connectToken, randomUUID(), createTimestamp(), appId); + return unsignedPathFactory.createProfilePath(appId, connectToken) + createSignatureParams(); } public String createAmlPath(String appId) { - return format(AML_PATH_TEMPLATE, appId, randomUUID(), createTimestamp()); + return unsignedPathFactory.createAmlPath(appId) + createSignatureParams(); } public String createDynamicSharingPath(String appId) { - return format(QR_CODE_PATH_TEMPLATE, appId, randomUUID(), createTimestamp()); + return unsignedPathFactory.createDynamicSharingPath(appId) + createSignatureParams(); } protected long createTimestamp() { diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java new file mode 100644 index 000000000..8fdf20457 --- /dev/null +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java @@ -0,0 +1,23 @@ +package com.yoti.api.client.spi.remote.call.factory; + +import static java.lang.String.format; + +public class UnsignedPathFactory { + + static final String PROFILE_PATH_TEMPLATE = "/profile/%s?appId=%s"; + static final String AML_PATH_TEMPLATE = "/aml-check?appId=%s"; + static final String QR_CODE_PATH_TEMPLATE = "/qrcodes/apps/%s"; + + public String createProfilePath(String appId, String connectToken) { + return format(PROFILE_PATH_TEMPLATE, connectToken, appId); + } + + public String createAmlPath(String appId) { + return format(AML_PATH_TEMPLATE, appId); + } + + public String createDynamicSharingPath(String appId) { + return format(QR_CODE_PATH_TEMPLATE, appId); + } + +} diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingService.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingService.java index 7e3c868ec..25f803bcc 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingService.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingService.java @@ -1,6 +1,5 @@ package com.yoti.api.client.spi.remote.call.qrcode; -import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_POST; import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_CHARSET; import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_YOTI_API_URL; import static com.yoti.api.client.spi.remote.call.YotiConstants.PROPERTY_YOTI_API_URL; @@ -9,17 +8,13 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; -import java.util.Map; import com.yoti.api.client.shareurl.DynamicScenario; import com.yoti.api.client.shareurl.DynamicShareException; -import com.yoti.api.client.spi.remote.call.JsonResourceFetcher; import com.yoti.api.client.spi.remote.call.ResourceException; -import com.yoti.api.client.spi.remote.call.ResourceFetcher; -import com.yoti.api.client.spi.remote.call.UrlConnector; -import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; -import com.yoti.api.client.spi.remote.call.factory.PathFactory; -import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; +import com.yoti.api.client.spi.remote.call.SignedRequest; +import com.yoti.api.client.spi.remote.call.SignedRequestBuilder; +import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; @@ -29,33 +24,26 @@ public final class DynamicSharingService { public static DynamicSharingService newInstance() { return new DynamicSharingService( - new PathFactory(), - new HeadersFactory(), + new UnsignedPathFactory(), new ObjectMapper(), - JsonResourceFetcher.newInstance(), - SignedMessageFactory.newInstance()); + SignedRequestBuilder.newInstance() + ); } private static final Logger LOG = LoggerFactory.getLogger(DynamicSharingService.class); - private final PathFactory pathFactory; - private final HeadersFactory headersFactory; + private final UnsignedPathFactory unsignedPathFactory; private final ObjectMapper objectMapper; - private final ResourceFetcher resourceFetcher; - private final SignedMessageFactory signedMessageFactory; + private final SignedRequestBuilder signedRequestBuilder; private final String apiUrl; - private DynamicSharingService(PathFactory pathFactory, - HeadersFactory headersFactory, + DynamicSharingService(UnsignedPathFactory unsignedPathFactory, ObjectMapper objectMapper, - ResourceFetcher resourceFetcher, - SignedMessageFactory signedMessageFactory) { - this.pathFactory = pathFactory; - this.headersFactory = headersFactory; + SignedRequestBuilder signedRequestBuilder) { + this.unsignedPathFactory = unsignedPathFactory; this.objectMapper = objectMapper; - this.resourceFetcher = resourceFetcher; - this.signedMessageFactory = signedMessageFactory; + this.signedRequestBuilder = signedRequestBuilder; apiUrl = System.getProperty(PROPERTY_YOTI_API_URL, DEFAULT_YOTI_API_URL); } @@ -65,16 +53,21 @@ public SimpleShareUrlResult createShareUrl(String appId, KeyPair keyPair, Dynami notNull(keyPair, "Application key Pair"); notNull(dynamicScenario, "Dynamic scenario"); - String path = pathFactory.createDynamicSharingPath(appId); + String path = unsignedPathFactory.createDynamicSharingPath(appId); LOG.info("Requesting Dynamic QR Code at {}", path); try { byte[] body = objectMapper.writeValueAsString(dynamicScenario).getBytes(DEFAULT_CHARSET); - String digest = signedMessageFactory.create(keyPair.getPrivate(), HTTP_POST, path, body); - Map headers = headersFactory.create(digest); - UrlConnector urlConnector = UrlConnector.get(apiUrl + path); - return resourceFetcher.postResource(urlConnector, body, headers, SimpleShareUrlResult.class); + SignedRequest signedRequest = this.signedRequestBuilder + .withKeyPair(keyPair) + .withBaseUrl(apiUrl) + .withEndpoint(path) + .withPayload(body) + .withHttpMethod("POST") + .build(); + + return signedRequest.execute(SimpleShareUrlResult.class); } catch (GeneralSecurityException ex) { throw new DynamicShareException("Error signing the request: ", ex); } catch (ResourceException ex) { diff --git a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java index e7b30dcfd..ee8838913 100644 --- a/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java +++ b/yoti-sdk-impl/src/main/java/com/yoti/api/client/spi/remote/util/Validation.java @@ -2,6 +2,8 @@ import static java.lang.String.format; +import java.util.List; + public class Validation { public static T notNull(T value, String name) { @@ -57,4 +59,10 @@ public static void matchesPattern(String value, String regex, String name) { } } + public static void withinList(T value, List allowedValues) { + if (!allowedValues.contains(value)) { + throw new IllegalArgumentException(format("value '%s' is not in the list '%s'", value, allowedValues)); + } + } + } diff --git a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/AnchorConverterTest.java b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/AnchorConverterTest.java index 57431c773..813652993 100644 --- a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/AnchorConverterTest.java +++ b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/AnchorConverterTest.java @@ -22,6 +22,7 @@ import com.yoti.api.client.spi.remote.proto.AttrProto; import com.yoti.api.client.spi.remote.util.AnchorType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.hamcrest.Description; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.hamcrest.collection.IsCollectionWithSize; @@ -31,7 +32,7 @@ public class AnchorConverterTest { static { - Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + Security.addProvider(new BouncyCastleProvider()); } private static final String PASSPORT_ISSUER = "CN=passport-registration-server"; diff --git a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/RemoteProfileServiceTest.java b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/RemoteProfileServiceTest.java index 614adcd68..ad787af68 100644 --- a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/RemoteProfileServiceTest.java +++ b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/RemoteProfileServiceTest.java @@ -1,38 +1,29 @@ package com.yoti.api.client.spi.remote.call; import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_GET; -import static com.yoti.api.client.spi.remote.call.YotiConstants.YOTI_API_PATH_PREFIX; +import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_YOTI_API_URL; import static com.yoti.api.client.spi.remote.util.CryptoUtil.KEY_PAIR_PEM; -import static com.yoti.api.client.spi.remote.util.CryptoUtil.base64; import static com.yoti.api.client.spi.remote.util.CryptoUtil.generateKeyPairFrom; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.HashMap; import java.util.Map; import com.yoti.api.client.ProfileException; -import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; -import com.yoti.api.client.spi.remote.call.factory.PathFactory; -import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; +import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; +import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -50,18 +41,15 @@ public class RemoteProfileServiceTest { private static final String APP_ID = "test-app"; private static final String TOKEN = "test-token"; private static final String GENERATED_PROFILE_PATH = "generatedProfilePath"; - private static final String SOME_SIGNATURE = "someSignature"; private static final Map SOME_HEADERS = new HashMap<>(); private static KeyPair KEY_PAIR; @InjectMocks RemoteProfileService testObj; - @Mock ResourceFetcher resourceFetcherMock; - @Mock PathFactory pathFactoryMock; - @Mock HeadersFactory headersFactoryMock; - @Mock SignedMessageFactory signedMessageFactoryMock; + @Mock UnsignedPathFactory unsignedPathFactory; + @Mock(answer = Answers.RETURNS_SELF) SignedRequestBuilder signedRequestBuilderMock; - @Captor ArgumentCaptor urlConnectorCaptor; + @Mock SignedRequest signedRequestMock; @BeforeClass public static void setUpClass() throws Exception { @@ -71,31 +59,27 @@ public static void setUpClass() throws Exception { @Before public void setUp() { - when(pathFactoryMock.createProfilePath(APP_ID, TOKEN)).thenReturn(GENERATED_PROFILE_PATH); - when(headersFactoryMock.create(SOME_SIGNATURE, base64(KEY_PAIR.getPublic().getEncoded()))).thenReturn(SOME_HEADERS); + when(unsignedPathFactory.createProfilePath(APP_ID, TOKEN)).thenReturn(GENERATED_PROFILE_PATH); } @Test public void shouldReturnReceiptForCorrectRequest() throws Exception { - when(signedMessageFactoryMock.create(KEY_PAIR.getPrivate(), HTTP_GET, GENERATED_PROFILE_PATH)).thenReturn(SOME_SIGNATURE); - when(resourceFetcherMock.fetchResource(any(UrlConnector.class), eq(SOME_HEADERS), eq(ProfileResponse.class))).thenReturn(PROFILE_RESPONSE); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(ProfileResponse.class)).thenReturn(PROFILE_RESPONSE); Receipt result = testObj.getReceipt(KEY_PAIR, APP_ID, TOKEN); - verify(resourceFetcherMock).fetchResource(urlConnectorCaptor.capture(), eq(SOME_HEADERS), eq(ProfileResponse.class)); - assertUrl(urlConnectorCaptor.getValue()); + verify(signedRequestBuilderMock).withKeyPair(KEY_PAIR); + verify(signedRequestBuilderMock).withBaseUrl(DEFAULT_YOTI_API_URL); + verify(signedRequestBuilderMock).withEndpoint(GENERATED_PROFILE_PATH); + verify(signedRequestBuilderMock).withHttpMethod(HTTP_GET); assertSame(RECEIPT, result); } - private void assertUrl(UrlConnector urlConnector) throws MalformedURLException { - URL url = new URL(urlConnector.getUrlString()); - assertEquals(YOTI_API_PATH_PREFIX + GENERATED_PROFILE_PATH, url.getPath()); - } - @Test public void shouldWrapSecurityExceptionInProfileException() throws Exception { GeneralSecurityException securityException = new GeneralSecurityException(); - when(signedMessageFactoryMock.create(KEY_PAIR.getPrivate(), HTTP_GET, GENERATED_PROFILE_PATH)).thenThrow(securityException); + when(signedRequestBuilderMock.build()).thenThrow(securityException); try { testObj.getReceipt(KEY_PAIR, APP_ID, TOKEN); @@ -108,8 +92,8 @@ public void shouldWrapSecurityExceptionInProfileException() throws Exception { @Test public void shouldThrowExceptionForIOError() throws Exception { IOException ioException = new IOException("Test exception"); - when(signedMessageFactoryMock.create(KEY_PAIR.getPrivate(), HTTP_GET, GENERATED_PROFILE_PATH)).thenReturn(SOME_SIGNATURE); - when(resourceFetcherMock.fetchResource(any(UrlConnector.class), eq(SOME_HEADERS), eq(ProfileResponse.class))).thenThrow(ioException); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(ProfileResponse.class)).thenThrow(ioException); try { testObj.getReceipt(KEY_PAIR, APP_ID, TOKEN); @@ -122,8 +106,8 @@ public void shouldThrowExceptionForIOError() throws Exception { @Test public void shouldThrowExceptionWithResourceExceptionCause() throws Throwable { ResourceException resourceException = new ResourceException(404, "Not Found", "Test exception"); - when(signedMessageFactoryMock.create(KEY_PAIR.getPrivate(), HTTP_GET, GENERATED_PROFILE_PATH)).thenReturn(SOME_SIGNATURE); - when(resourceFetcherMock.fetchResource(any(UrlConnector.class), eq(SOME_HEADERS), eq(ProfileResponse.class))).thenThrow(resourceException); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(ProfileResponse.class)).thenThrow(resourceException); try { testObj.getReceipt(KEY_PAIR, APP_ID, TOKEN); diff --git a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilderTest.java b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilderTest.java new file mode 100644 index 000000000..0c634b39c --- /dev/null +++ b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilderTest.java @@ -0,0 +1,233 @@ +package com.yoti.api.client.spi.remote.call; + +import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_GET; +import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_YOTI_API_URL; +import static com.yoti.api.client.spi.remote.call.YotiConstants.DIGEST_HEADER; +import static com.yoti.api.client.spi.remote.util.CryptoUtil.KEY_PAIR_PEM; +import static com.yoti.api.client.spi.remote.util.CryptoUtil.generateKeyPairFrom; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.collection.IsMapContaining.hasEntry; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.Security; +import java.util.HashMap; +import java.util.Map; + +import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; +import com.yoti.api.client.spi.remote.call.factory.PathFactory; +import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SignedRequestBuilderTest { + + private static final String SOME_ENDPOINT = "/someEndpoint"; + private static final String SOME_SIGNATURE = "someSignature"; + private static final Map SIGNED_REQUEST_HEADERS; + private static final String SOME_BASE_URL = "someBaseUrl"; + private static final byte[] SOME_BYTES = "someBytes".getBytes(); + private static final String SIGNATURE_PARAMS_STRING = "nonce=someNonce×tamp=someTimestamp"; + private static KeyPair KEY_PAIR; + + static { + Security.addProvider(new BouncyCastleProvider()); + + SIGNED_REQUEST_HEADERS = new HashMap<>(); + SIGNED_REQUEST_HEADERS.put(YotiConstants.DIGEST_HEADER, SOME_SIGNATURE); + } + + @BeforeClass + public static void setUpClass() throws Exception { + KEY_PAIR = generateKeyPairFrom(KEY_PAIR_PEM); + } + + @InjectMocks SignedRequestBuilder signedRequestBuilder; + + @Mock PathFactory pathFactoryMock; + @Mock SignedMessageFactory signedMessageFactoryMock; + @Mock HeadersFactory headersFactoryMock; + @Captor ArgumentCaptor pathCaptor; + + @Test + public void withHttpMethod_shouldThrowExceptionWhenSuppliedWithUnsupportedHttpMethod() throws Exception { + try { + signedRequestBuilder.withHttpMethod("someNonsenseHere"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("someNonsenseHere")); + return; + } + fail("Expected an IllegalArgumentException"); + + } + + @Test + public void build_shouldThrowExceptionWhenMissingKeyPair() throws Exception { + try { + signedRequestBuilder.build(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("keyPair")); + return; + } + fail("Expected an IllegalArgumentException"); + } + + @Test + public void build_shouldThrowExceptionWhenMissingBaseUrl() throws Exception { + try { + signedRequestBuilder.withKeyPair(KEY_PAIR) + .build(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("baseUrl")); + return; + } + fail("Expected an IllegalArgumentException"); + } + + @Test + public void build_shouldThrowExceptionWhenMissingEndpoint() throws Exception { + try { + signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(SOME_BASE_URL) + .build(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("endpoint")); + return; + } + fail("Expected an IllegalArgumentException"); + } + + @Test + public void build_shouldThrowExceptionWhenMissingHttpMethod() throws Exception { + try { + signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(SOME_BASE_URL) + .withEndpoint(SOME_ENDPOINT) + .build(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("httpMethod")); + return; + } + fail("Expected an IllegalArgumentException"); + } + + @Test + public void build_shouldCreateSignedRequestWithCustomQueryParameter() throws Exception { + when(pathFactoryMock.createSignatureParams()).thenReturn(SIGNATURE_PARAMS_STRING); + + SignedRequest result = signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(SOME_BASE_URL) + .withEndpoint(SOME_ENDPOINT) + .withHttpMethod(HTTP_GET) + .withQueryParameter("someQueryParam", "someParamValue") + .build(); + + assertThat(result.getUri().getQuery(), containsString(SIGNATURE_PARAMS_STRING)); + } + + @Test + public void build_shouldCreateSignedRequestWithProvidedHttpHeader() throws Exception { + SignedRequest result = signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(DEFAULT_YOTI_API_URL) + .withEndpoint(SOME_ENDPOINT) + .withHttpMethod(HTTP_GET) + .withHeader("myCustomHeader", "customHeaderValue") + .build(); + + assertThat(result.getHeaders(), hasEntry("myCustomHeader", "customHeaderValue")); + } + + @Test + public void build_shouldIgnoreAnyProvidedSignedRequestHeaders() throws Exception { + when(pathFactoryMock.createSignatureParams()).thenReturn(SIGNATURE_PARAMS_STRING); + when(signedMessageFactoryMock.create(any(PrivateKey.class), anyString(), anyString())).thenReturn(SOME_SIGNATURE); + when(headersFactoryMock.create(SOME_SIGNATURE)).thenReturn(SIGNED_REQUEST_HEADERS); + + SignedRequest result = signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(DEFAULT_YOTI_API_URL) + .withEndpoint(SOME_ENDPOINT) + .withHttpMethod(HTTP_GET) + .withHeader(DIGEST_HEADER, "customHeaderValue") + .build(); + + assertThat(result.getHeaders(), hasEntry(DIGEST_HEADER, SOME_SIGNATURE)); + } + + @Test + public void shouldRemoveTrailingSlashesFromBaseUrl() throws Exception { + SignedRequest simpleSignedRequest = signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(SOME_BASE_URL + "////////////") + .withEndpoint(SOME_ENDPOINT) + .withHttpMethod(HTTP_GET) + .build(); + + assertThat(simpleSignedRequest.getUri().toString(), containsString(SOME_BASE_URL + SOME_ENDPOINT)); + } + + @Test + public void shouldCreateSignedRequestSuccessfullyWithRequiredFieldsOnly() throws Exception { + when(pathFactoryMock.createSignatureParams()).thenReturn(SIGNATURE_PARAMS_STRING); + when(signedMessageFactoryMock.create(any(PrivateKey.class), anyString(), anyString())).thenReturn(SOME_SIGNATURE); + when(headersFactoryMock.create(SOME_SIGNATURE)).thenReturn(SIGNED_REQUEST_HEADERS); + + SignedRequest result = signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(SOME_BASE_URL) + .withEndpoint(SOME_ENDPOINT) + .withHttpMethod(HTTP_GET) + .build(); + + verify(signedMessageFactoryMock).create(eq(KEY_PAIR.getPrivate()), eq(HTTP_GET), pathCaptor.capture()); + assertThat(pathCaptor.getValue(), is(SOME_ENDPOINT + "?" + SIGNATURE_PARAMS_STRING)); + assertThat(result.getUri().toString(), is(SOME_BASE_URL + pathCaptor.getValue())); + assertThat(result.getMethod(), is(HTTP_GET)); + assertThat(result.getHeaders().get(DIGEST_HEADER), containsString(SOME_SIGNATURE)); + assertThat(result.getHeaders(), hasEntry(DIGEST_HEADER, SOME_SIGNATURE)); + assertTrue(result.getData() == null); + } + + @Test + public void shouldCreatedSignedRequestSuccessfullyWithAllProperties() throws Exception { + when(pathFactoryMock.createSignatureParams()).thenReturn(SIGNATURE_PARAMS_STRING); + when(signedMessageFactoryMock.create(any(PrivateKey.class), anyString(), anyString(), any(byte[].class))).thenReturn(SOME_SIGNATURE); + when(headersFactoryMock.create(SOME_SIGNATURE)).thenReturn(SIGNED_REQUEST_HEADERS); + + SignedRequest result = signedRequestBuilder.withKeyPair(KEY_PAIR) + .withBaseUrl(SOME_BASE_URL) + .withEndpoint(SOME_ENDPOINT) + .withHttpMethod(HTTP_GET) + .withQueryParameter("someQueryParam", "someParamValue") + .withHeader("myCustomHeader", "customHeaderValue") + .withPayload(SOME_BYTES) + .build(); + + verify(signedMessageFactoryMock).create(eq(KEY_PAIR.getPrivate()), eq(HTTP_GET), pathCaptor.capture(), eq(SOME_BYTES)); + assertThat(pathCaptor.getValue(), is(SOME_ENDPOINT + "?someQueryParam=someParamValue&" + SIGNATURE_PARAMS_STRING)); + assertThat(result.getUri().toString(), is(SOME_BASE_URL + pathCaptor.getValue())); + assertThat(result.getMethod(), is(HTTP_GET)); + assertThat(result.getHeaders().get(DIGEST_HEADER), containsString(SOME_SIGNATURE)); + assertThat(result.getHeaders(), hasEntry(DIGEST_HEADER, SOME_SIGNATURE)); + assertThat(result.getHeaders(), hasEntry("myCustomHeader", "customHeaderValue")); + assertArrayEquals(SOME_BYTES, result.getData()); + } + +} diff --git a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlServiceTest.java b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlServiceTest.java index c6dae1344..8fd0cf73f 100644 --- a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlServiceTest.java +++ b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/aml/RemoteAmlServiceTest.java @@ -3,19 +3,16 @@ import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_POST; -import static com.yoti.api.client.spi.remote.call.YotiConstants.YOTI_API_PATH_PREFIX; +import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_YOTI_API_URL; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; import static org.mockito.Answers.RETURNS_DEEP_STUBS; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Answers.RETURNS_SELF; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; -import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.HashMap; @@ -24,11 +21,9 @@ import com.yoti.api.client.AmlException; import com.yoti.api.client.aml.AmlProfile; import com.yoti.api.client.spi.remote.call.ResourceException; -import com.yoti.api.client.spi.remote.call.ResourceFetcher; -import com.yoti.api.client.spi.remote.call.UrlConnector; -import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; -import com.yoti.api.client.spi.remote.call.factory.PathFactory; -import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; +import com.yoti.api.client.spi.remote.call.SignedRequest; +import com.yoti.api.client.spi.remote.call.SignedRequestBuilder; +import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -36,8 +31,6 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -49,20 +42,17 @@ public class RemoteAmlServiceTest { private static final String GENERATED_PATH = "generatedPath"; private static final String SERIALIZED_BODY = "serializedBody"; private static final byte[] BODY_BYTES = SERIALIZED_BODY.getBytes(); - private static final String SOME_SIGNATURE = "someSignature"; private static final Map SOME_HEADERS = new HashMap<>(); @InjectMocks RemoteAmlService testObj; - @Mock PathFactory pathFactoryMock; - @Mock HeadersFactory headersFactoryMock; + @Mock UnsignedPathFactory unsignedPathFactoryMock; @Mock ObjectMapper objectMapperMock; - @Mock ResourceFetcher resourceFetcherMock; - @Mock SignedMessageFactory signedMessageFactoryMock; + @Mock(answer = RETURNS_SELF) SignedRequestBuilder signedRequestBuilderMock; @Mock AmlProfile amlProfileMock; @Mock(answer = RETURNS_DEEP_STUBS) KeyPair keyPairMock; - @Captor ArgumentCaptor urlConnectorCaptor; + @Mock SignedRequest signedRequestMock; @Mock SimpleAmlResult simpleAmlResultMock; @BeforeClass @@ -72,34 +62,31 @@ public static void setUpClass() { @Before public void setUp() { - when(pathFactoryMock.createAmlPath(SOME_APP_ID)).thenReturn(GENERATED_PATH); - when(headersFactoryMock.create(SOME_SIGNATURE)).thenReturn(SOME_HEADERS); + when(unsignedPathFactoryMock.createAmlPath(SOME_APP_ID)).thenReturn(GENERATED_PATH); } @Test public void shouldPerformAmlCheck() throws Exception { when(objectMapperMock.writeValueAsString(amlProfileMock)).thenReturn(SERIALIZED_BODY); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, GENERATED_PATH, BODY_BYTES)).thenReturn(SOME_SIGNATURE); - when(resourceFetcherMock.postResource(any(UrlConnector.class), eq(BODY_BYTES), eq(SOME_HEADERS), eq(SimpleAmlResult.class))) - .thenReturn(simpleAmlResultMock); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(SimpleAmlResult.class)).thenReturn(simpleAmlResultMock); SimpleAmlResult result = testObj.performCheck(keyPairMock, SOME_APP_ID, amlProfileMock); - verify(resourceFetcherMock).postResource(urlConnectorCaptor.capture(), eq(BODY_BYTES), eq(SOME_HEADERS), eq(SimpleAmlResult.class)); - assertEquals(YOTI_API_PATH_PREFIX + GENERATED_PATH, getPath(urlConnectorCaptor.getValue())); + verify(signedRequestBuilderMock).withKeyPair(keyPairMock); + verify(signedRequestBuilderMock).withBaseUrl(DEFAULT_YOTI_API_URL); + verify(signedRequestBuilderMock).withEndpoint(GENERATED_PATH); + verify(signedRequestBuilderMock).withPayload(BODY_BYTES); + verify(signedRequestBuilderMock).withHttpMethod(HTTP_POST); assertSame(result, simpleAmlResultMock); } - private String getPath(UrlConnector urlConnector) throws Exception { - return new URL(urlConnector.getUrlString()).getPath(); - } - @Test public void shouldWrapIOException() throws Exception { IOException ioException = new IOException(); when(objectMapperMock.writeValueAsString(amlProfileMock)).thenReturn(SERIALIZED_BODY); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, GENERATED_PATH, BODY_BYTES)).thenReturn(SOME_SIGNATURE); - when(resourceFetcherMock.postResource(any(UrlConnector.class), eq(BODY_BYTES), eq(SOME_HEADERS), eq(SimpleAmlResult.class))).thenThrow(ioException); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(SimpleAmlResult.class)).thenThrow(ioException); try { testObj.performCheck(keyPairMock, SOME_APP_ID, amlProfileMock); @@ -113,9 +100,8 @@ public void shouldWrapIOException() throws Exception { public void shouldWrapResourceException() throws Exception { ResourceException resourceException = new ResourceException(HTTP_UNAUTHORIZED, "Unauthorized", "failed verification"); when(objectMapperMock.writeValueAsString(amlProfileMock)).thenReturn(SERIALIZED_BODY); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, GENERATED_PATH, BODY_BYTES)).thenReturn(SOME_SIGNATURE); - when(resourceFetcherMock.postResource(any(UrlConnector.class), eq(BODY_BYTES), eq(SOME_HEADERS), eq(SimpleAmlResult.class))) - .thenThrow(resourceException); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(SimpleAmlResult.class)).thenThrow(resourceException); try { testObj.performCheck(keyPairMock, SOME_APP_ID, amlProfileMock); @@ -129,7 +115,7 @@ public void shouldWrapResourceException() throws Exception { public void shouldWrapGeneralSecurityException() throws Exception { GeneralSecurityException generalSecurityException = new GeneralSecurityException(); when(objectMapperMock.writeValueAsString(amlProfileMock)).thenReturn(SERIALIZED_BODY); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, GENERATED_PATH, BODY_BYTES)).thenThrow(generalSecurityException); + when(signedRequestBuilderMock.build()).thenThrow(generalSecurityException); try { testObj.performCheck(keyPairMock, SOME_APP_ID, amlProfileMock); diff --git a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java index 90a932821..e5eb89e62 100644 --- a/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java +++ b/yoti-sdk-impl/src/test/java/com/yoti/api/client/spi/remote/call/qrcode/DynamicSharingServiceTest.java @@ -1,21 +1,15 @@ package com.yoti.api.client.spi.remote.call.qrcode; -import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_YOTI_API_URL; -import static com.yoti.api.client.spi.remote.call.HttpMethod.HTTP_POST; -import static com.yoti.api.client.spi.remote.call.YotiConstants.YOTI_API_PATH_PREFIX; - -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; import static org.mockito.Answers.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; +import static org.mockito.Answers.RETURNS_SELF; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; -import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.HashMap; @@ -23,12 +17,11 @@ import com.yoti.api.client.shareurl.DynamicScenario; import com.yoti.api.client.shareurl.DynamicShareException; +import com.yoti.api.client.spi.remote.call.HttpMethod; import com.yoti.api.client.spi.remote.call.ResourceException; -import com.yoti.api.client.spi.remote.call.ResourceFetcher; -import com.yoti.api.client.spi.remote.call.UrlConnector; -import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; -import com.yoti.api.client.spi.remote.call.factory.PathFactory; -import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; +import com.yoti.api.client.spi.remote.call.SignedRequest; +import com.yoti.api.client.spi.remote.call.SignedRequestBuilder; +import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -36,8 +29,6 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -49,22 +40,18 @@ public class DynamicSharingServiceTest { private static final String DYNAMIC_QRCODE_PATH = "dynamicQRCodePath"; private static final String SOME_BODY = "someBody"; private static final byte[] SOME_BODY_BYTES = SOME_BODY.getBytes(); - private static final String SOME_SIGNATURE = "someSignature"; private static final Map SOME_HEADERS = new HashMap<>(); @InjectMocks DynamicSharingService testObj; - @Mock PathFactory pathFactoryMock; - @Mock HeadersFactory headersFactoryMock; + @Mock UnsignedPathFactory unsignedPathFactoryMock; @Mock ObjectMapper objectMapperMock; - @Mock ResourceFetcher resourceFetcherMock; - @Mock SignedMessageFactory signedMessageFactoryMock; + @Mock(answer = RETURNS_SELF) SignedRequestBuilder signedRequestBuilderMock; @Mock DynamicScenario simpleDynamicScenarioMock; - @Mock SimpleShareUrlResult simpleShareUrlResultMock; + @Mock SignedRequest signedRequestMock; @Mock(answer = RETURNS_DEEP_STUBS) KeyPair keyPairMock; - - @Captor ArgumentCaptor urlConnectorCaptor; + @Mock SimpleShareUrlResult simpleShareUrlResultMock; @BeforeClass public static void setUpClass() { @@ -73,8 +60,7 @@ public static void setUpClass() { @Before public void setUp() { - when(pathFactoryMock.createDynamicSharingPath(APP_ID)).thenReturn(DYNAMIC_QRCODE_PATH); - when(headersFactoryMock.create(SOME_SIGNATURE)).thenReturn(SOME_HEADERS); + when(unsignedPathFactoryMock.createDynamicSharingPath(APP_ID)).thenReturn(DYNAMIC_QRCODE_PATH); } @Test(expected = IllegalArgumentException.class) @@ -109,7 +95,7 @@ public void shouldThrowDynamicShareExceptionWhenParsingFails() throws Exception public void shouldWrapSecurityExceptionInDynamicShareException() throws Exception { when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenReturn(SOME_BODY); GeneralSecurityException securityException = new GeneralSecurityException(); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES)).thenThrow(securityException); + when(signedRequestBuilderMock.build()).thenThrow(securityException); try { testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock); @@ -122,9 +108,9 @@ public void shouldWrapSecurityExceptionInDynamicShareException() throws Exceptio @Test public void shouldThrowExceptionForIOError() throws Exception { when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenReturn(SOME_BODY); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES)).thenReturn(SOME_SIGNATURE); IOException ioException = new IOException(); - when(resourceFetcherMock.postResource(any(UrlConnector.class), eq(SOME_BODY_BYTES), eq(SOME_HEADERS), eq(SimpleShareUrlResult.class))).thenThrow(ioException); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(SimpleShareUrlResult.class)).thenThrow(ioException); try { testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock); @@ -137,34 +123,32 @@ public void shouldThrowExceptionForIOError() throws Exception { @Test public void shouldThrowExceptionWithResourceExceptionCause() throws Exception { when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenReturn(SOME_BODY); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES)).thenReturn(SOME_SIGNATURE); - ResourceException resourceEx = new ResourceException(HTTP_NOT_FOUND, "Not found", "Test exception"); - when(resourceFetcherMock.postResource(any(UrlConnector.class), eq(SOME_BODY_BYTES), eq(SOME_HEADERS), eq(SimpleShareUrlResult.class))).thenThrow(resourceEx); + ResourceException resourceException = new ResourceException(404, "Not Found", "Test exception"); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(SimpleShareUrlResult.class)).thenThrow(resourceException); try { testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock); fail("Expected a DynamicShareException"); } catch (DynamicShareException ex) { - assertSame(resourceEx, ex.getCause()); + assertSame(resourceException, ex.getCause()); } } @Test public void shouldReturnReceiptForCorrectRequest() throws Exception { when(objectMapperMock.writeValueAsString(simpleDynamicScenarioMock)).thenReturn(SOME_BODY); - when(signedMessageFactoryMock.create(keyPairMock.getPrivate(), HTTP_POST, DYNAMIC_QRCODE_PATH, SOME_BODY_BYTES)).thenReturn(SOME_SIGNATURE); - when(resourceFetcherMock.postResource(any(UrlConnector.class), eq(SOME_BODY_BYTES), eq(SOME_HEADERS), eq(SimpleShareUrlResult.class))) - .thenReturn(simpleShareUrlResultMock); + when(signedRequestBuilderMock.build()).thenReturn(signedRequestMock); + when(signedRequestMock.execute(SimpleShareUrlResult.class)).thenReturn(simpleShareUrlResultMock); SimpleShareUrlResult result = testObj.createShareUrl(APP_ID, keyPairMock, simpleDynamicScenarioMock); - verify(resourceFetcherMock).postResource(urlConnectorCaptor.capture(), eq(SOME_BODY_BYTES), eq(SOME_HEADERS), eq(SimpleShareUrlResult.class)); - assertEquals(YOTI_API_PATH_PREFIX + DYNAMIC_QRCODE_PATH, getPath(urlConnectorCaptor.getValue())); + verify(signedRequestBuilderMock).withKeyPair(keyPairMock); + verify(signedRequestBuilderMock).withBaseUrl(DEFAULT_YOTI_API_URL); + verify(signedRequestBuilderMock).withEndpoint(DYNAMIC_QRCODE_PATH); + verify(signedRequestBuilderMock).withPayload(SOME_BODY_BYTES); + verify(signedRequestBuilderMock).withHttpMethod(HttpMethod.HTTP_POST); assertSame(simpleShareUrlResultMock, result); } - private String getPath(UrlConnector urlConnector) throws Exception { - return new URL(urlConnector.getUrlString()).getPath(); - } - }