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();
- }
-
}