Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fit-connect/sdk-java
1 result
Show changes
Showing
with 1171 additions and 594 deletions
## Client Module
### Commandline Client
The sdk comes with a commandline client to be able to use the sdk without any coding.
The sdk provides a commandline-client as a runnable .jar, to be able to use the sdk without any coding.
#### Setup & Build
1. Build project root wih ``./mvnw clean package``
2. Go to client/target and find a runnable jar ``client-VERSION.jar``
2. Go to client/target and find a runnable jar ``fit-connect-client.jar``
3. Provide [config yaml](../config.yml):
1. set environment variable ``FIT_CONNECT_CONFIG``:
1. Linux/MacOS: ``export FIT_CONNECT_CONFIG=path/to/config.yml``
......@@ -16,13 +16,13 @@ The sdk comes with a commandline client to be able to use the sdk without any co
var senderClient = ClientFactory.senderClient(config);
````
3. put ``config.yml`` in same path as the .jar-file
5. run client with ``java -jar client-VERSION.jar [COMMAND] [OPTIONS]``
5. run client with ``java -jarfit-connect-client.jar [COMMAND] [OPTIONS]``
#### SEND Example
#### SEND Single Submission Example
The send command submits a new submission to a destination. Apart from optional attachments, all options are mandatory.
````sh
java -jar client-1.0-SNAPSHOT.jar send
java -jar fit-connect-client.jar send
--destinationId=1b7d1a24-a6c8-4050-bb71-ae3749ec432f
--serviceName=Test
--leikaKey=urn:de:fim:leika:leistung:99400048079000
......@@ -35,7 +35,7 @@ java -jar client-1.0-SNAPSHOT.jar send
#### LIST Submissions Example
The list command lists all submissionIds for a given destinationID.
````sh
java -jar client-1.0-SNAPSHOT.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f
java -jar fit-connect-client.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f
````
#### GET Single Submission Example
The get command loads a submission by `submissionId` and stores data and attachments in the given target location. If no target is
......@@ -43,13 +43,13 @@ set, the cmd-client saves the data into in a folder named by the `submissionId`
jar.
````sh
java -jar client-1.0-SNAPSHOT.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
java -jar fit-connect-client.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
````
#### Batch Mode
To send multiple submission with a single command, the client can be used in batch mode:
````sh
java -jar client-1.0-SNAPSHOT.jar batch --data=batch_data.csv
java -jar fit-connect-client.jar batch --data=batch_data.csv
````
Currently, the import of CSV is supported. Follow the schema below, setting up your data:
......@@ -112,95 +112,3 @@ Usage: <main class> [command] [command options]
* --data
Path to submission data as csv
````
### API Flow
The ClientFactory provides fluent API clients for both **Sender** and **Subscriber**.
As the flow chart below shows, the fluent client guides through all essential calls in order to hand in a correct
**submission** as well as receive submissions on the subscriber side.
#### Api client flow for sending a submission
For the actual sender client those calls look like this:
<table>
<tr>
<th>Workflow</th>
<th>Java sample calls</th>
</tr>
<tr>
<td>
```mermaid
flowchart TD
A[Create Client] --> B(Add Attachments)
A[Create Client] --> C(Add Data)
B -->|next| C[Add Data]
C -->|next| D[Add Destination]
D -->|next| E[Add ServiceType]
E -->|send| F[SubmissionForPickup]
```
Add Service Type
</td>
<td>
```java
ClientFactory.senderClient()
.withAttachments(attachments)
.withJsonData("{ caseSpecific: 'Data' }")
.withDestination(UUID.randomUUID())
.withServiceType("ServiceName", "leika:key:service")
.submit();
```
</td>
</tr>
</table>
#### Api client flow for subscribing to a submission
<table>
<tr>
<th>Workflow</th>
<th>Java sample calls</th>
</tr>
<tr>
<td>
```mermaid
flowchart TD
A[Create Client] --> B(Poll List ofAvailable Submissions)
A[Create Client] --> C(Request Submission by ID)
C -->|get| D[Attachments]
C -->|get| E[Metadata]
C -->|get| F[Data]
```
</td>
<td>
```java
var client = ClientFactory.subscriberClient();
var submissions = client.getAvailableSubmissions(destinationId);
// filter submission list for requested one ...
var submission = client.requestSubmission(submissionId)
submission.getAttachments();
submission.getMetadata();
submission.getData();
```
</td>
</tr>
</table>
package dev.fitko.fitconnect.client;
import dev.fitko.fitconnect.api.domain.model.route.Area;
import dev.fitko.fitconnect.api.domain.model.route.Route;
import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import dev.fitko.fitconnect.api.exceptions.RoutingException;
import dev.fitko.fitconnect.api.services.routing.RoutingService;
import dev.fitko.fitconnect.client.router.DestinationSearch;
import dev.fitko.fitconnect.core.routing.RouteVerifier;
import java.util.List;
public final class RoutingClient {
private final RoutingService routingService;
private final RouteVerifier routeVerifier;
public RoutingClient(final RoutingService routingService, final RouteVerifier routeVerifier) {
this.routingService = routingService;
this.routeVerifier = routeVerifier;
}
/**
* Finds a list of {@link Route}s by a given service identifier and an area search criterion.
*
* @param search search parameters that contain leikaKey and one other area search criterion see {@link DestinationSearch}
* @return list of found {@link Route}s matching the search criteria
* @throws RoutingException if the signature validation failed
* @throws RestApiException if a technical error occurred during the query
*/
public List<Route> findDestinations(final DestinationSearch search) throws RoutingException, RestApiException {
final RouteResult routeResult = routingService.getRoutes(search.getLeikaKey(), search.getArs(), search.getAgs(), search.getAreaId(), search.getOffset(), search.getLimit());
final ValidationResult result = routeVerifier.validateRouteDestinations(routeResult.getRoutes(), search.getLeikaKey(), search.getArs());
if (result.hasError()) {
throw new RoutingException(result.getError().getMessage(), result.getError());
}
return routeResult.getRoutes();
}
/**
* Finds a list of {@link Area}s based on a filter criterion, e.g. a zip code or city.
*
* @param filter filter criterion as string
* @param offset offset to start from
* @param limit max entries
* @return list of {@link Area}
* @throws RestApiException if a technical error occurred during the query
*/
public List<Area> findAreas(final String filter, final int offset, final int limit) throws RestApiException {
return findAreas(List.of(filter), offset, limit);
}
/**
* Find a list {@link Area}s based on a list of multiple filter criteria, e.g. a zip code or a city as in List.of("04229", "Leip*").
*
* @param filters list of string filters
* @param offset offset to start from
* @param limit max entries
* @return list of {@link Area}
* @throws RestApiException if a technical error occurred during the query
*/
public List<Area> findAreas(final List<String> filters, final int offset, final int limit) throws RestApiException {
return routingService.getAreas(filters, offset, limit).getAreas();
}
}
......@@ -21,7 +21,7 @@ import java.util.Optional;
import java.util.UUID;
/**
* A fluent client for announcing and handing in a {@link SubmitSubmission}
* A sender-side client for announcing and handing in a {@link SubmitSubmission}
*/
public class SenderClient {
......@@ -77,7 +77,7 @@ public class SenderClient {
* @param timestamp timestamp provided by the callback
* @param httpBody HTTP body provided by the callback
* @param callbackSecret secret owned by the client, which is used to calculate the hmac
* @return {@code true} if hmac and timestamp provided by the callback meet the required conditions
* @return {@code ValidationResult.ok()} if hmac and timestamp provided by the callback meet the required conditions
*/
public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
return sender.validateCallback(hmac, timestamp, httpBody, callbackSecret);
......
......@@ -31,11 +31,10 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildDecryptedAttachmentPayload;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.getDataHashFromMetadata;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.mapToReceivedAttachments;
/**
* A subscriber-side client for retrieving submissions.
......@@ -144,17 +143,27 @@ public class SubscriberClient {
return null;
}
public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
return this.subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret);
public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
return subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret);
}
private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List<DecryptedAttachmentPayload> attachments) {
final MimeType mimeType = metadata.getContentStructure().getData().getSubmissionSchema().getMimeType();
final ReceivedData receivedData = new ReceivedData(new String(decryptedData, StandardCharsets.UTF_8), mimeType);
final List<ReceivedAttachment> receivedAttachments = mapToReceivedAttachments(attachments);
final List<ReceivedAttachment> receivedAttachments = attachments.stream().map(mapToReceivedAttachment()).collect(Collectors.toList());
return new ReceivedSubmission(subscriber, submission, metadata, receivedData, receivedAttachments);
}
private static Function<DecryptedAttachmentPayload, ReceivedAttachment> mapToReceivedAttachment() {
return payload -> ReceivedAttachment.builder()
.attachmentId(payload.getAttachmentMetadata().getAttachmentId())
.filename(payload.getAttachmentMetadata().getFilename())
.mimeType(payload.getAttachmentMetadata().getMimeType())
.description(payload.getAttachmentMetadata().getDescription())
.data(payload.getDecryptedContent())
.build();
}
private List<DecryptedAttachmentPayload> loadAttachments(final UUID submissionId, final List<Attachment> attachmentMetadata) {
if (attachmentMetadata == null || attachmentMetadata.isEmpty()) {
LOGGER.info("Submission contains no attachments");
......@@ -164,7 +173,11 @@ public class SubscriberClient {
for (final Attachment metadata : attachmentMetadata) {
final String encryptedAttachment = downloadAttachment(submissionId, metadata);
final byte[] decryptedAttachment = decryptAttachment(metadata, encryptedAttachment);
receivedAttachments.add(buildDecryptedAttachmentPayload(metadata, decryptedAttachment));
final DecryptedAttachmentPayload decryptedAttachmentPayload = DecryptedAttachmentPayload.builder()
.decryptedContent(decryptedAttachment)
.attachmentMetadata(metadata)
.build();
receivedAttachments.add(decryptedAttachmentPayload);
}
return receivedAttachments;
}
......@@ -207,4 +220,10 @@ public class SubscriberClient {
return ValidationResult.ok();
}
private String getDataHashFromMetadata(final Metadata metadata) {
return metadata.getContentStructure()
.getData()
.getHash()
.getContent();
}
}
......@@ -52,11 +52,16 @@ public final class ApplicationConfigLoader {
public static ApplicationConfig loadConfigFromYaml(final String configYaml) {
final Yaml applicationPropertiesYaml = new Yaml(new Constructor(ApplicationConfig.class));
ApplicationConfig applicationConfig = applicationPropertiesYaml.load(configYaml);
final ApplicationConfig applicationConfig = applicationPropertiesYaml.load(configYaml);
return applicationConfig;
}
/**
* Load BuildInfo properties.
*
* @return {@link BuildInfo}
*/
public static BuildInfo loadBuildInfo() {
final Yaml buildInfoYaml = new Yaml(new Constructor(BuildInfo.class));
......
......@@ -6,6 +6,7 @@ import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.BuildInfo;
import dev.fitko.fitconnect.api.config.SchemaConfig;
import dev.fitko.fitconnect.api.config.SubscriberConfig;
import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
import dev.fitko.fitconnect.api.exceptions.InitializationException;
import dev.fitko.fitconnect.api.exceptions.InvalidKeyException;
import dev.fitko.fitconnect.api.services.Sender;
......@@ -13,13 +14,15 @@ import dev.fitko.fitconnect.api.services.Subscriber;
import dev.fitko.fitconnect.api.services.auth.OAuthService;
import dev.fitko.fitconnect.api.services.crypto.CryptoService;
import dev.fitko.fitconnect.api.services.crypto.MessageDigestService;
import dev.fitko.fitconnect.api.services.keys.KeyService;
import dev.fitko.fitconnect.api.services.events.EventLogService;
import dev.fitko.fitconnect.api.services.events.EventLogVerificationService;
import dev.fitko.fitconnect.api.services.events.SecurityEventService;
import dev.fitko.fitconnect.api.services.keys.KeyService;
import dev.fitko.fitconnect.api.services.routing.RoutingService;
import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
import dev.fitko.fitconnect.api.services.submission.SubmissionService;
import dev.fitko.fitconnect.api.services.validation.ValidationService;
import dev.fitko.fitconnect.client.RoutingClient;
import dev.fitko.fitconnect.client.SenderClient;
import dev.fitko.fitconnect.client.SubscriberClient;
import dev.fitko.fitconnect.core.SubmissionSender;
......@@ -27,11 +30,13 @@ import dev.fitko.fitconnect.core.SubmissionSubscriber;
import dev.fitko.fitconnect.core.auth.DefaultOAuthService;
import dev.fitko.fitconnect.core.crypto.HashService;
import dev.fitko.fitconnect.core.crypto.JWECryptoService;
import dev.fitko.fitconnect.core.keys.PublicKeyService;
import dev.fitko.fitconnect.core.events.EventLogApiService;
import dev.fitko.fitconnect.core.events.EventLogVerifier;
import dev.fitko.fitconnect.core.events.SecurityEventTokenService;
import dev.fitko.fitconnect.core.http.ProxyConfig;
import dev.fitko.fitconnect.core.http.RestService;
import dev.fitko.fitconnect.core.keys.PublicKeyService;
import dev.fitko.fitconnect.core.routing.RouteVerifier;
import dev.fitko.fitconnect.core.routing.RoutingApiService;
import dev.fitko.fitconnect.core.schema.SchemaResourceProvider;
import dev.fitko.fitconnect.core.submission.SubmissionApiService;
import dev.fitko.fitconnect.core.validation.DefaultValidationService;
......@@ -46,7 +51,7 @@ import java.text.ParseException;
import java.util.List;
/**
* Factory that constructs clients for {@link Sender} and {@link Subscriber}.
* Factory that constructs {@link SenderClient}, {@link SubscriberClient} and {@link RoutingClient}.
*/
public final class ClientFactory {
......@@ -54,13 +59,15 @@ public final class ClientFactory {
private static final String CONFIG_ENV_KEY_NAME = "FIT_CONNECT_CONFIG";
private static final String SET_SCHEMA_DIR = "/set-schema";
private static final String DESTINATION_SCHEMA_DIR = "/destination-schema";
private static final String METADATA_SCHEMA_DIR = "/metadata-schema";
private ClientFactory() {
}
/**
* Create a new {@link SenderClient} that is automatically configured via the config.yml file.
* Create a new {@link SenderClient} to send submissions that is automatically configured via the config.yml file.
*
* @return the sender client
*/
......@@ -69,7 +76,7 @@ public final class ClientFactory {
}
/**
* Create a new {@link SenderClient} that is automatically configured via a provided {@link ApplicationConfig}.
* Create a new {@link SenderClient} to send submissions that is automatically configured via a provided {@link ApplicationConfig}.
*
* @return the sender client
*/
......@@ -80,7 +87,7 @@ public final class ClientFactory {
/**
* Create a new {@link SubscriberClient} that is automatically configured via config.yml file.
* Create a new {@link SubscriberClient} to receive submissions that is automatically configured via config.yml file.
*
* @return the subscriber client
*/
......@@ -89,7 +96,7 @@ public final class ClientFactory {
}
/**
* Create a new {@link SubscriberClient} that is automatically configured via a provided {@link ApplicationConfig}.
* Create a new {@link SubscriberClient} to receive submissions that is automatically configured via a provided {@link ApplicationConfig}.
*
* @return the subscriber client
*/
......@@ -105,41 +112,73 @@ public final class ClientFactory {
return new SubscriberClient(subscriber, privateKey);
}
private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) {
/**
* Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}.
*
* @return the routing client
*/
public static RoutingClient routingClient() {
return routingClient(loadConfig());
}
/**
* Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}.
*
* @return the routing client
*/
public static RoutingClient routingClient(final ApplicationConfig config) {
LOGGER.info("Initializing routing client ...");
final RestTemplate restTemplate = getRestTemplate(config, ApplicationConfigLoader.loadBuildInfo());
final SchemaProvider schemaProvider = getSchemaProvider();
final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
final MessageDigestService messageDigestService = getMessageDigestService();
final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
final RouteVerifier routeVerifier = getRouteVerifier(keyService, validator);
final RoutingService routingService = getRoutingService(config, restTemplate);
return new RoutingClient(routingService, routeVerifier);
}
private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) {
final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
final SchemaProvider schemaProvider = getSchemaProvider();
final MessageDigestService messageDigestService = getMessageDigestService();
final CryptoService cryptoService = getCryptoService(messageDigestService);
final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
final OAuthService authService = getSubscriberConfiguredAuthService(config, restTemplate);
final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator);
final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService);
final SecurityEventService setService = getSecurityEventTokenService(config, validator);
return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService);
return new SubmissionSender(submissionService, eventLogService, cryptoService, validator, keyService);
}
private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) {
private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) {
final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
final SchemaProvider schemaProvider = getSchemaProvider();
final MessageDigestService messageDigestService = getMessageDigestService();
final CryptoService cryptoService = getCryptoService(messageDigestService);
final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
final OAuthService authService = getSubscriberConfiguredAuthService(config, restTemplate);
final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator);
final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService);
final SecurityEventService setService = getSecurityEventTokenService(config, validator);
return new SubmissionSender(submissionService, eventLogService, cryptoService, validator, keyService);
return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService);
}
private static OAuthService getSenderConfiguredAuthService(final ApplicationConfig config, final RestTemplate restTemplate) {
final String clientId = config.getSenderConfig().getClientId();
final String clientSecret = config.getSenderConfig().getClientSecret();
......@@ -169,8 +208,8 @@ public final class ClientFactory {
}
private static RestTemplate getRestTemplate(final ApplicationConfig config, final BuildInfo buildInfo) {
final ProxyConfig proxyConfig = new ProxyConfig(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo);
return proxyConfig.proxyRestTemplate();
final RestService restService = new RestService(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo);
return restService.getRestTemplate();
}
private static MessageDigestService getMessageDigestService() {
......@@ -193,9 +232,19 @@ public final class ClientFactory {
}
private static SchemaProvider getSchemaProvider() {
final List<String> metadataSchemaFiles = SchemaConfig.getMetadataSchemaFileNames(METADATA_SCHEMA_DIR);
final List<String> setSchemaFiles = SchemaConfig.getSetSchemaFilePaths(SET_SCHEMA_DIR);
return new SchemaResourceProvider(setSchemaFiles, metadataSchemaFiles);
final List<String> metadataSchemaFiles = SchemaConfig.getMetadataSchemaFileNames(METADATA_SCHEMA_DIR);
final List<String> destinationSchemaFiles = SchemaConfig.getDestinationSchemaPaths(DESTINATION_SCHEMA_DIR);
final SchemaResources schemaResources = new SchemaResources(setSchemaFiles, metadataSchemaFiles, destinationSchemaFiles);
return new SchemaResourceProvider(schemaResources);
}
private static RoutingService getRoutingService(final ApplicationConfig config, final RestTemplate restTemplate) {
return new RoutingApiService(config, restTemplate);
}
private static RouteVerifier getRouteVerifier(final KeyService keyService, final ValidationService validationService) {
return new RouteVerifier(keyService, validationService);
}
private static RSAKey readRSAKeyFromString(final String key) {
......
package dev.fitko.fitconnect.client.router;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.util.regex.Pattern;
/**
* Builds a new search request for a service identifier and AT MAX. ONE OTHER search criterion (ars | ags | areaId).
*/
@Value
@AllArgsConstructor
public class DestinationSearch {
String leikaKey;
String ars;
String ags;
String areaId;
int offset;
int limit;
public static Builder Builder() {
return new Builder();
}
public static class Builder {
private static final Pattern AGS_PATTERN = Pattern.compile("^(\\d{2}|\\d{3}|\\d{5}|\\d{8})$");
private static final Pattern ARS_PATTERN = Pattern.compile("^(\\d{2}|\\d{3}|\\d{5}|\\d{9}|\\d{12})$");
private static final Pattern AREA_ID_PATTERN = Pattern.compile("^\\d{1,}");
private static final Pattern LEIKA_KEY_PATTERN = Pattern.compile("^99\\d{12}$");
private String leikaKey;
private String ars;
private String ags;
private String areaId;
private int offset = 0;
private int limit = 100;
/**
* Leika Key of the requested service.
*
* @param leikaKey service identifier
* @return Builder
* @throws IllegalArgumentException if the leika key pattern is not matching
*/
public Builder withLeikaKey(final String leikaKey) throws IllegalArgumentException {
if (!LEIKA_KEY_PATTERN.matcher(leikaKey).matches()) {
throw new IllegalArgumentException("Leika key does not match allowed pattern " + LEIKA_KEY_PATTERN);
}
this.leikaKey = leikaKey;
return this;
}
/**
* Official municipal code of the place.
*
* @param ars amtlicher regionalschlüssel
* @return Builder
* @throws IllegalArgumentException if the ars key pattern is not matching
*/
public Builder withArs(final String ars) throws IllegalArgumentException{
if (!ARS_PATTERN.matcher(ars).matches()) {
throw new IllegalArgumentException("ARS key does not match allowed pattern " + ARS_PATTERN);
}
this.ars = ars;
return this;
}
/**
* Official regional key of the area.
*
* @param ags amtlicher gemeindeschlüssel
* @return Builder
* @throws IllegalArgumentException if the ags key pattern is not matching
*/
public Builder withAgs(final String ags) throws IllegalArgumentException {
if (!AGS_PATTERN.matcher(ags).matches()) {
throw new IllegalArgumentException("AGS key does not match allowed pattern " + AGS_PATTERN);
}
this.ags = ags;
return this;
}
/**
* ID of the area. This ID can be determined via the routing clients <code>findAreas</code> search.
*
* @param areaId id of the area
* @return Builder
* @see dev.fitko.fitconnect.client.RoutingClient#findAreas(String, int, int)
* @throws IllegalArgumentException if the area id pattern is not matching
*/
public Builder withAreaId(final String areaId) throws IllegalArgumentException {
if (!AREA_ID_PATTERN.matcher(areaId).matches()) {
throw new IllegalArgumentException("AreaId key does not match allowed pattern " + AREA_ID_PATTERN);
}
this.areaId = areaId;
return this;
}
/**
* Start position of the subset of the result set. Default is 0.
*
* @param offset start of the subset
* @return Builder
* @throws IllegalArgumentException if the offset is a negative number
*/
public Builder withOffset(final int offset) throws IllegalArgumentException{
if (limit < 0) {
throw new IllegalArgumentException("offset must be positive");
}
this.offset = offset;
return this;
}
/**
* Max. size of the subset of the result set. Maximum is 500. Default is 100.
*
* @param limit max. entries in the subset
* @return Builder
* @throws IllegalArgumentException if the limit is > 500
*/
public Builder withLimit(final int limit) throws IllegalArgumentException {
if (limit > 500) {
throw new IllegalArgumentException("limit must no be > 500");
}
this.limit = limit;
return this;
}
/**
* Construct the search request.
*
* @return DestinationSearch
*/
public DestinationSearch build() throws IllegalArgumentException {
if(leikaKey == null){
throw new IllegalArgumentException("leikaKey is mandatory");
}
return new DestinationSearch(leikaKey, ars, ags, areaId, offset, limit);
}
}
}
......@@ -2,6 +2,7 @@ package dev.fitko.fitconnect.client.sender.strategies;
import dev.fitko.fitconnect.api.domain.model.submission.CreateSubmission;
import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
......@@ -14,12 +15,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildCreateSubmission;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSentSubmission;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmissionToAnnounce;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmitSubmission;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.toAttachmentPayloads;
public class SendEncryptedSubmissionStrategy {
......@@ -34,9 +36,9 @@ public class SendEncryptedSubmissionStrategy {
public SentSubmission send(final EncryptedSubmissionPayload submissionPayload) {
try {
final UUID submissionId = announceNewSubmission(submissionPayload);
final SubmitSubmission submitSubmission = buildSubmitSubmission(submissionId, submissionPayload);
final List<AttachmentPayload> attachmentPayloads = toAttachmentPayloads(submissionPayload.getEncryptedAttachments());
final UUID submissionId = announceNewSubmission(submissionPayload, attachmentPayloads);
final SubmitSubmission submitSubmission = buildSubmitSubmission(submissionId, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedMetadata());
final var startTimeAttachmentUpload = StopWatch.start();
uploadAttachments(attachmentPayloads, submissionId);
......@@ -47,7 +49,7 @@ public class SendEncryptedSubmissionStrategy {
LOGGER.info("Uploading submission took {}", StopWatch.stopWithFormattedTime(startTimeSubmissionUpload));
LOGGER.info("SUCCESSFULLY HANDED IN SUBMISSION !");
return buildSentSubmission(submissionPayload, attachmentPayloads, submission);
return buildSentSubmission(attachmentPayloads, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedData(), submission);
} catch (final RestApiException e) {
LOGGER.error("Sending submission failed", e);
......@@ -58,8 +60,10 @@ public class SendEncryptedSubmissionStrategy {
return null;
}
private UUID announceNewSubmission(final EncryptedSubmissionPayload submissionPayload) {
final CreateSubmission submissionToAnnounce = buildSubmissionToAnnounce(submissionPayload);
private UUID announceNewSubmission(final EncryptedSubmissionPayload submissionPayload, final List<AttachmentPayload> attachmentPayloads) {
final UUID destinationId = submissionPayload.getDestinationId();
final ServiceType serviceType = submissionPayload.getServiceType();
final CreateSubmission submissionToAnnounce = buildCreateSubmission(destinationId, attachmentPayloads, serviceType);
return sender.createSubmission(submissionToAnnounce).getSubmissionId();
}
......@@ -71,4 +75,14 @@ public class SendEncryptedSubmissionStrategy {
attachmentPayloads.forEach(a -> sender.uploadAttachment(submissionId, a.getAttachmentId(), a.getEncryptedData()));
}
}
private List<AttachmentPayload> toAttachmentPayloads(final Map<UUID, String> encryptedAttachments) {
return encryptedAttachments.entrySet()
.stream()
.map(attachment -> AttachmentPayload.builder()
.encryptedData(attachment.getValue())
.attachmentId(attachment.getKey())
.build())
.collect(Collectors.toList());
}
}
......@@ -2,7 +2,14 @@ package dev.fitko.fitconnect.client.sender.strategies;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
import dev.fitko.fitconnect.api.domain.model.metadata.ContentStructure;
import dev.fitko.fitconnect.api.domain.model.metadata.Hash;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType;
import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
......@@ -34,20 +41,18 @@ import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildMetadataToSend;
import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildCreateSubmission;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSentSubmission;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmissionToAnnounce;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmitSubmission;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.createData;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.getFileAttachmentPayload;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.getSchemaUriForMimeType;
import static dev.fitko.fitconnect.client.util.SubmissionUtil.getSubmissionSchemasFromDestination;
public class SendNewSubmissionStrategy {
......@@ -70,7 +75,7 @@ public class SendNewSubmissionStrategy {
final Destination destination = sender.getDestination(destinationId);
final List<AttachmentPayload> encryptedAttachments = encryptAndHashAttachments(encryptionKey, submissionPayload.getAttachments());
final CreateSubmission newSubmission = buildSubmissionToAnnounce(destinationId, serviceType, encryptedAttachments);
final CreateSubmission newSubmission = buildCreateSubmission(destinationId, encryptedAttachments, serviceType);
final SubmissionForPickup announcedSubmission = sender.createSubmission(newSubmission);
final UUID announcedSubmissionId = announcedSubmission.getSubmissionId();
......@@ -119,20 +124,74 @@ public class SendNewSubmissionStrategy {
private Metadata buildMetadata(final SubmissionPayload submissionPayload, final Destination destination, final List<AttachmentPayload> encryptedAttachments) {
final String hashedData = sender.createHash(submissionPayload.getData().getBytes(StandardCharsets.UTF_8));
final URI schemaUri = getSchemaUri(submissionPayload.getDataMimeType(), destination);
final Data data = createData(submissionPayload, hashedData, schemaUri);
return buildMetadataToSend(data, submissionPayload.getServiceType(), encryptedAttachments);
final MimeType dataMimeType = submissionPayload.getDataMimeType();
final URI schemaUri = getSubmissionSchemaFromDestination(destination, dataMimeType);
final Data data = createData(dataMimeType, hashedData, schemaUri);
final PublicServiceType publicServiceType = buildPublicServiceType(submissionPayload.getServiceType());
final List<Attachment> attachmentMetadata = encryptedAttachments.stream().map(this::toHashedAttachment).collect(Collectors.toList());
final var contentStructure = new ContentStructure();
contentStructure.setAttachments(attachmentMetadata);
contentStructure.setData(data);
final var metadata = new Metadata();
metadata.setSchema(METADATA_V_1_0_0.toString());
metadata.setContentStructure(contentStructure);
metadata.setPublicServiceType(publicServiceType);
return metadata;
}
private Data createData(final MimeType mimeType, final String hashedData, final URI schemaUri) {
final var hash = new Hash();
hash.setContent(hashedData);
hash.setSignatureType(SignatureType.SHA_512);
final var submissionSchema = new SubmissionSchema();
submissionSchema.setMimeType(mimeType);
submissionSchema.setSchemaUri(schemaUri);
final var data = new Data();
data.setSubmissionSchema(submissionSchema);
data.setHash(hash);
return data;
}
private URI getSchemaUri(final MimeType mimeType, final Destination destination) {
final List<SubmissionSchema> submissionSchemas = getSubmissionSchemasFromDestination(destination);
final String supportedSchemas = submissionSchemas.stream().map(s -> s.getMimeType().toString()).collect(Collectors.joining(" | "));
final Optional<URI> schemaUriForMimeType = getSchemaUriForMimeType(submissionSchemas, mimeType);
if (schemaUriForMimeType.isEmpty()) {
LOGGER.error("Destination supports the mime-type '{}'. Mime-type {} is not allowed, please check the destination", supportedSchemas, mimeType);
throw new SchemaNotFoundException("Schema for mime-type " + mimeType + " not found");
private PublicServiceType buildPublicServiceType(final ServiceType serviceType) {
final var publicServiceType = new PublicServiceType();
publicServiceType.setIdentifier(serviceType.getIdentifier());
if (serviceType.getName() != null) {
publicServiceType.setName(serviceType.getName());
}
return schemaUriForMimeType.get();
if (serviceType.getDescription() != null) {
publicServiceType.setDescription(serviceType.getDescription());
}
return publicServiceType;
}
private Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) {
final var attachment = new Attachment();
attachment.setAttachmentId(attachmentPayload.getAttachmentId());
attachment.setPurpose(Purpose.ATTACHMENT);
attachment.setFilename(attachmentPayload.getFile().getName());
attachment.setMimeType(attachmentPayload.getMimeType());
final var hash = new Hash();
hash.setContent(attachmentPayload.getHashedData());
hash.setSignatureType(SignatureType.SHA_512);
attachment.setHash(hash);
return attachment;
}
private URI getSubmissionSchemaFromDestination(final Destination destination, final MimeType mimeType) {
return destination.getServices().stream()
.map(DestinationService::getSubmissionSchemas)
.flatMap(Collection::stream)
.filter(schema -> schema.getMimeType().equals(mimeType))
.map(SubmissionSchema::getSchemaUri)
.findFirst()
.orElseThrow(() -> new SchemaNotFoundException("Destination does not support data with mime-type " + mimeType));
}
private void uploadAttachments(final List<AttachmentPayload> attachmentPayloads, final UUID submissionId) {
......@@ -146,7 +205,7 @@ public class SendNewSubmissionStrategy {
private List<AttachmentPayload> encryptAndHashAttachments(final RSAKey encryptionKey, final List<File> attachments) {
return attachments.stream()
.map(getFileAttachmentPayload())
.map(file -> AttachmentPayload.builder().file(file).attachmentId(UUID.randomUUID()).build())
.filter(Objects::nonNull)
.map(payload -> encryptAndHashAttachment(encryptionKey, payload))
.collect(Collectors.toList());
......@@ -160,12 +219,13 @@ public class SendNewSubmissionStrategy {
final String hashedBytes = sender.createHash(rawData);
return attachmentPayload.withEncryptedData(encryptedAttachment)
.withHashedData(hashedBytes)
.withMimeType(setMimeType(attachmentPayload));
.withMimeType(detectMimeTypeFromFile(attachmentPayload));
} catch (final Exception e) {
throw new AttachmentCreationException("Attachment '" + file.getAbsolutePath() + "' could not be created ", e);
}
}
private String setMimeType(final AttachmentPayload attachmentPayload) {
private String detectMimeTypeFromFile(final AttachmentPayload attachmentPayload) {
final File file = attachmentPayload.getFile();
String mimeType;
try {
......
package dev.fitko.fitconnect.client.util;
import com.nimbusds.jose.JWEObject;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
import dev.fitko.fitconnect.api.domain.model.metadata.*;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
import dev.fitko.fitconnect.api.domain.model.submission.*;
import dev.fitko.fitconnect.api.domain.model.submission.CreateSubmission;
import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
import dev.fitko.fitconnect.api.domain.model.submission.Submission;
import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission;
import dev.fitko.fitconnect.client.sender.model.AttachmentPayload;
import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload;
import dev.fitko.fitconnect.client.sender.model.SubmissionPayload;
import dev.fitko.fitconnect.client.subscriber.model.DecryptedAttachmentPayload;
import dev.fitko.fitconnect.client.subscriber.model.ReceivedAttachment;
import java.io.File;
import java.net.URI;
import java.text.ParseException;
import java.util.*;
import java.util.function.Function;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
/**
* Helper to create types for announced, sent and submit submissions.
*/
public final class SubmissionUtil {
private SubmissionUtil() {
}
private static CreateSubmission createSubmission(final UUID destinationId, final ServiceType serviceType, final List<UUID> attachmentIdsToAnnounce) {
public static CreateSubmission buildCreateSubmission(final UUID destinationId, final List<AttachmentPayload> attachments, final ServiceType serviceType) {
return CreateSubmission.builder()
.destinationId(destinationId)
.announcedAttachments(attachmentIdsToAnnounce)
.announcedAttachments(attachments.stream().map(AttachmentPayload::getAttachmentId).collect(Collectors.toList()))
.serviceType(serviceType)
.build();
}
public static PublicServiceType buildPublicServiceType(final ServiceType serviceType) {
final var publicServiceType = new PublicServiceType();
publicServiceType.setIdentifier(serviceType.getIdentifier());
if (serviceType.getName() != null) {
publicServiceType.setName(serviceType.getName());
}
if (serviceType.getDescription() != null) {
publicServiceType.setDescription(serviceType.getDescription());
}
return publicServiceType;
}
public static Metadata buildMetadataToSend(final Data data, final ServiceType serviceType, final List<AttachmentPayload> encryptedAttachments) {
final PublicServiceType publicServiceType = buildPublicServiceType(serviceType);
final List<Attachment> attachmentMetadata = toAttachmentMetadata(encryptedAttachments);
return buildMetadata(attachmentMetadata, data, publicServiceType);
}
public static Metadata buildMetadata(final List<Attachment> attachments, final Data data, final PublicServiceType publicServiceType) {
final var contentStructure = new ContentStructure();
contentStructure.setAttachments(attachments);
contentStructure.setData(data);
final var metadata = new Metadata();
metadata.setSchema(METADATA_V_1_0_0.toString());
metadata.setContentStructure(contentStructure);
metadata.setPublicServiceType(publicServiceType);
return metadata;
}
public static CreateSubmission buildSubmissionToAnnounce(final EncryptedSubmissionPayload submissionPayload) {
final ServiceType serviceType = submissionPayload.getServiceType();
final List<UUID> attachmentIds = new ArrayList<>(submissionPayload.getEncryptedAttachments().keySet());
return createSubmission(submissionPayload.getDestinationId(), serviceType, attachmentIds);
}
public static SubmitSubmission buildSubmitSubmission(final UUID submissionId, final EncryptedSubmissionPayload submissionPayload) {
final SubmitSubmission submission = new SubmitSubmission();
submission.setSubmissionId(submissionId);
submission.setEncryptedData(submissionPayload.getEncryptedData());
submission.setEncryptedMetadata(submissionPayload.getEncryptedMetadata());
return submission;
}
public static SubmitSubmission buildSubmitSubmission(final UUID submissionId, final String encryptedData, final String encryptedMetadata) {
final SubmitSubmission submission = new SubmitSubmission();
submission.setSubmissionId(submissionId);
......@@ -91,83 +39,6 @@ public final class SubmissionUtil {
return submission;
}
private static Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) {
final var attachment = new Attachment();
attachment.setAttachmentId(attachmentPayload.getAttachmentId());
attachment.setPurpose(Purpose.ATTACHMENT);
attachment.setFilename(attachmentPayload.getFile().getName());
attachment.setMimeType(attachmentPayload.getMimeType());
final var hash = new Hash();
hash.setContent(attachmentPayload.getHashedData());
hash.setSignatureType(SignatureType.SHA_512);
attachment.setHash(hash);
return attachment;
}
private static List<UUID> toAttachmentIds(final List<AttachmentPayload> attachmentPayloads) {
return attachmentPayloads.stream()
.map(AttachmentPayload::getAttachmentId)
.collect(Collectors.toList());
}
public static List<Attachment> toAttachmentMetadata(final List<AttachmentPayload> attachmentPayloads) {
return attachmentPayloads.stream()
.map(SubmissionUtil::toHashedAttachment)
.collect(Collectors.toList());
}
private static Function<Map.Entry<UUID, String>, AttachmentPayload> getEncryptedAttachmentPayload() {
return attachment -> AttachmentPayload.builder()
.encryptedData(attachment.getValue())
.attachmentId(attachment.getKey())
.build();
}
public static Function<File, AttachmentPayload> getFileAttachmentPayload() {
return file -> AttachmentPayload.builder()
.file(file)
.attachmentId(UUID.randomUUID())
.build();
}
public static List<AttachmentPayload> toAttachmentPayloads(final Map<UUID, String> encryptedAttachments) {
return encryptedAttachments.entrySet()
.stream()
.map(getEncryptedAttachmentPayload())
.collect(Collectors.toList());
}
public static CreateSubmission buildSubmissionToAnnounce(final UUID destinationId, final ServiceType serviceType, final List<AttachmentPayload> encryptedAttachments) {
final List<UUID> attachmentIdsToAnnounce = toAttachmentIds(encryptedAttachments);
return createSubmission(destinationId, serviceType, attachmentIdsToAnnounce);
}
public static List<SubmissionSchema> getSubmissionSchemasFromDestination(final Destination destination) {
return destination.getServices().stream()
.map(DestinationService::getSubmissionSchemas)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
public static Optional<URI> getSchemaUriForMimeType(final List<SubmissionSchema> submissionSchemas, final MimeType mimeType) {
return submissionSchemas.stream()
.filter(schema -> schema.getMimeType().equals(mimeType))
.map(SubmissionSchema::getSchemaUri)
.findFirst();
}
public static DecryptedAttachmentPayload buildDecryptedAttachmentPayload(final Attachment attachmentMetadata, final byte[] decryptedAttachment) {
return DecryptedAttachmentPayload.builder()
.decryptedContent(decryptedAttachment)
.attachmentMetadata(attachmentMetadata)
.build();
}
public static SentSubmission buildSentSubmission(final EncryptedSubmissionPayload submissionPayload, final List<AttachmentPayload> encryptedAttachments, final Submission submission) {
return buildSentSubmission(encryptedAttachments, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedMetadata(), submission);
}
public static SentSubmission buildSentSubmission(final List<AttachmentPayload> encryptedAttachments, final String encryptedData, final String encryptedMetadata, final Submission submission) {
final AuthenticationTags authenticationTags = new AuthenticationTags();
......@@ -183,45 +54,6 @@ public final class SubmissionUtil {
.build();
}
public static List<ReceivedAttachment> mapToReceivedAttachments(final List<DecryptedAttachmentPayload> decryptedAttachmentPayloads) {
return decryptedAttachmentPayloads.stream()
.map(SubmissionUtil::mapToReceivedAttachment)
.collect(Collectors.toList());
}
public static Data createData(final SubmissionPayload submissionPayload, final String hashedData, final URI schemaUri) {
final var hash = new Hash();
hash.setContent(hashedData);
hash.setSignatureType(SignatureType.SHA_512);
final var submissionSchema = new SubmissionSchema();
submissionSchema.setMimeType(submissionPayload.getDataMimeType());
submissionSchema.setSchemaUri(schemaUri);
final var data = new Data();
data.setSubmissionSchema(submissionSchema);
data.setHash(hash);
return data;
}
public static String getDataHashFromMetadata(final Metadata metadata) {
return metadata.getContentStructure()
.getData()
.getHash()
.getContent();
}
private static ReceivedAttachment mapToReceivedAttachment(final DecryptedAttachmentPayload payload) {
final Attachment metadata = payload.getAttachmentMetadata();
return ReceivedAttachment.builder()
.attachmentId(metadata.getAttachmentId())
.filename(metadata.getFilename())
.mimeType(metadata.getMimeType())
.description(metadata.getDescription())
.data(payload.getDecryptedContent())
.build();
}
private static Map<UUID, String> getAuthTagsFromAttachments(final List<AttachmentPayload> encryptedAttachments) {
return encryptedAttachments.stream().collect(Collectors.toMap(AttachmentPayload::getAttachmentId, SubmissionUtil::getAuthTag));
}
......@@ -237,5 +69,4 @@ public final class SubmissionUtil {
return "";
}
}
}
package dev.fitko.fitconnect.client.util;
import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.services.Sender;
import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload;
import dev.fitko.fitconnect.client.sender.model.SubmissionPayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
public class ValidDataGuard {
private static final Logger LOGGER = LoggerFactory.getLogger(ValidDataGuard.class);
private static final Pattern LEIKA_KEY_PATTERN = Pattern.compile("^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$");
private final Sender sender;
......@@ -22,57 +21,87 @@ public class ValidDataGuard {
this.sender = sender;
}
/**
* Checks if the encrypted submission payload is in a valid state for sending.
*
* @param encryptedSubmissionPayload payload to be checked
* @throws IllegalArgumentException if one of the checks fails
* @throws IllegalStateException if the destination does not have a declared service type
*/
public void ensureValidDataPayload(final EncryptedSubmissionPayload encryptedSubmissionPayload) {
if (encryptedSubmissionPayload == null) {
throw new IllegalArgumentException("Encrypted payload must not be null.");
}
if (encryptedSubmissionPayload.getEncryptedData() == null) {
throw new IllegalArgumentException("Encrypted data is mandatory, but was null.");
}
if (encryptedSubmissionPayload.getEncryptedMetadata() == null) {
throw new IllegalArgumentException("Encrypted metadata must not be null.");
}
testDefaults(encryptedSubmissionPayload.getDestinationId(), encryptedSubmissionPayload.getEncryptedData(), encryptedSubmissionPayload.getServiceType());
testDefaults(encryptedSubmissionPayload.getDestinationId(), encryptedSubmissionPayload.getServiceType());
}
/**
* Checks if the unencrypted submission payload is in a valid state for sending.
*
* @param submissionPayload payload to be checked
* @throws IllegalArgumentException if one of the checks fails
*/
public void ensureValidDataPayload(final SubmissionPayload submissionPayload) {
if (submissionPayload == null) {
throw new IllegalArgumentException("Payload must not be null.");
}
if (isDataFormatInvalid(submissionPayload)) {
throw new IllegalArgumentException("Data format is invalid, please provide well-formed data.");
if (submissionPayload.getData() == null) {
throw new IllegalArgumentException("Data is mandatory, but was null.");
}
testDefaults(submissionPayload.getDestinationId(), submissionPayload.getData(), submissionPayload.getServiceType());
testOnValidDataFormat(submissionPayload);
testDefaults(submissionPayload.getDestinationId(), submissionPayload.getServiceType());
}
public void testDefaults(final UUID destinationId, final String data, final ServiceType serviceType) {
private void testDefaults(final UUID destinationId, final ServiceType serviceType) {
if (destinationId == null) {
throw new IllegalArgumentException("DestinationId is mandatory, but was null.");
} else if (data == null) {
throw new IllegalArgumentException("Data is mandatory, but was null.");
} else if (serviceType == null) {
throw new IllegalArgumentException("ServiceType is mandatory, but was null.");
} else if (serviceType.getIdentifier() == null) {
throw new IllegalArgumentException("Leika key is mandatory, but was null.");
} else if (noValidLeikaKeyPattern(serviceType.getIdentifier())) {
throw new IllegalArgumentException("LeikaKey has invalid format, please follow: ^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$.");
} else if (serviceTypeDoesNotMatchDestination(destinationId, serviceType)) {
throw new IllegalArgumentException("Provided service type '" + serviceType.getIdentifier() + "' is not allowed by the destination ");
}
}
private boolean isDataFormatInvalid(final SubmissionPayload submissionPayload) {
private boolean serviceTypeDoesNotMatchDestination(final UUID destinationId, final ServiceType serviceType) {
return sender.getDestination(destinationId).getServices().stream()
.map(DestinationService::getIdentifier)
.filter(Objects::nonNull)
.filter(serviceIdentifier -> serviceIdentifier.equals(serviceType.getIdentifier()))
.findFirst()
.isEmpty();
}
private void testOnValidDataFormat(final SubmissionPayload submissionPayload) {
final MimeType dataMimeType = submissionPayload.getDataMimeType();
if (dataMimeType.equals(MimeType.APPLICATION_JSON)) {
final ValidationResult validationResult = sender.validateJsonFormat(submissionPayload.getData());
if (validationResult.hasError()) {
LOGGER.error("Data is not in expected json format, please provide valid json {}", validationResult.getError().getMessage());
return true;
}
checkJsonFormat(submissionPayload);
} else if (dataMimeType.equals(MimeType.APPLICATION_XML)) {
final ValidationResult validationResult = sender.validateXmlFormat(submissionPayload.getData());
if (validationResult.hasError()) {
LOGGER.error("Data is not in expected xml format, please provide valid xml {}", validationResult.getError().getMessage());
return true;
}
checkXmlFormat(submissionPayload);
}
}
private void checkXmlFormat(final SubmissionPayload submissionPayload) {
final ValidationResult validationResult = sender.validateXmlFormat(submissionPayload.getData());
if (validationResult.hasError()) {
throw new IllegalArgumentException("Data is not in expected xml format, please provide valid xml: " + validationResult.getError().getMessage());
}
}
private void checkJsonFormat(final SubmissionPayload submissionPayload) {
final ValidationResult validationResult = sender.validateJsonFormat(submissionPayload.getData());
if (validationResult.hasError()) {
throw new IllegalArgumentException("Data is not in expected json format, please provide valid json: " + validationResult.getError().getMessage());
}
return false;
}
private boolean noValidLeikaKeyPattern(final String leikaKey) {
......
productName: "${project.name}"
productVersion: "${project.version}"
productVersion: "${project.version}${local-version-suffix}"
commit: "${git.commit.id.full}"
\ No newline at end of file
......@@ -2,37 +2,46 @@ package dev.fitko.fitconnect.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.RSAKey;
import dev.fitko.fitconnect.api.config.*;
import dev.fitko.fitconnect.api.config.ApplicationConfig;
import dev.fitko.fitconnect.api.config.BuildInfo;
import dev.fitko.fitconnect.api.config.Environment;
import dev.fitko.fitconnect.api.config.EnvironmentName;
import dev.fitko.fitconnect.api.config.SenderConfig;
import dev.fitko.fitconnect.api.config.SubscriberConfig;
import dev.fitko.fitconnect.api.domain.auth.OAuthToken;
import dev.fitko.fitconnect.api.domain.model.event.Event;
import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry;
import dev.fitko.fitconnect.api.domain.model.event.EventStatus;
import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog;
import dev.fitko.fitconnect.api.domain.model.metadata.ContentStructure;
import dev.fitko.fitconnect.api.domain.model.metadata.Hash;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType;
import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
import dev.fitko.fitconnect.api.domain.model.route.Area;
import dev.fitko.fitconnect.api.domain.model.route.Route;
import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
import dev.fitko.fitconnect.api.exceptions.RestApiException;
import dev.fitko.fitconnect.api.services.crypto.CryptoService;
import dev.fitko.fitconnect.client.factory.ClientFactory;
import dev.fitko.fitconnect.client.router.DestinationSearch;
import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder;
import dev.fitko.fitconnect.client.sender.SubmissionBuilder;
import dev.fitko.fitconnect.client.sender.model.AttachmentPayload;
import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission;
import dev.fitko.fitconnect.client.util.SubmissionUtil;
import dev.fitko.fitconnect.core.auth.DefaultOAuthService;
import dev.fitko.fitconnect.core.crypto.HashService;
import dev.fitko.fitconnect.core.crypto.JWECryptoService;
import dev.fitko.fitconnect.core.http.RestService;
import org.apache.tika.mime.MimeTypes;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.IOException;
......@@ -41,28 +50,44 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.ParseException;
import java.time.Duration;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.*;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* This test uses real credentials and endpoints stored in gitlab ci variables to test the sdk against a real system
*/
@EnabledIfEnvironmentVariable(named="TEST_DESTINATION_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named="SENDER_CLIENT_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named="SENDER_CLIENT_SECRET", matches = ".*")
@EnabledIfEnvironmentVariable(named="SUBSCRIBER_CLIENT_ID", matches = ".*")
@EnabledIfEnvironmentVariable(named="SUBSCRIBER_CLIENT_SECRET", matches = ".*")
class ClientIntegrationTest {
private static final String authBaseUrl = "https://auth-testing.fit-connect.fitko.dev";
private static final String submissionBaseUrl = "https://submission-api-testing.fit-connect.fitko.dev";
@Nested
class SendSubmissionTests {
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testSendAndConfirmCycle() {
// Given
......@@ -93,7 +118,6 @@ class ClientIntegrationTest {
}
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testSendAndConfirmCycleWithEncryptedData() throws ParseException, IOException {
// Given
......@@ -116,19 +140,19 @@ class ClientIntegrationTest {
final String encryptedData = cryptoService.encryptString(encryptionKey, jsonData);
final String encryptedAttachment = cryptoService.encryptString(encryptionKey, attachmentData);
final SubmissionSchema submissionSchema = new SubmissionSchema();
final var submissionSchema = new SubmissionSchema();
submissionSchema.setSchemaUri(URI.create("https://schema.fitko.de/fim/s00000000009_1.0.0.schema.json"));
submissionSchema.setMimeType(MimeType.APPLICATION_JSON);
final Hash hash = new Hash();
hash.setSignatureType(SignatureType.SHA_512);
hash.setContent(cryptoService.hashBytes(jsonData.getBytes(StandardCharsets.UTF_8)));
final var dataHash = new Hash();
dataHash.setSignatureType(SignatureType.SHA_512);
dataHash.setContent(cryptoService.hashBytes(jsonData.getBytes(StandardCharsets.UTF_8)));
final Data data = new Data();
data.setHash(hash);
final var data = new Data();
data.setHash(dataHash);
data.setSubmissionSchema(submissionSchema);
final AttachmentPayload attachmentPayload = AttachmentPayload.builder()
final var attachmentPayload = AttachmentPayload.builder()
.encryptedData(encryptedAttachment)
.file(attachmentFile)
.mimeType(MimeTypes.PLAIN_TEXT)
......@@ -136,14 +160,29 @@ class ClientIntegrationTest {
.hashedData(cryptoService.hashBytes(attachmentData.getBytes(StandardCharsets.UTF_8)))
.build();
final ServiceType serviceType = ServiceType.builder()
.name("Test Service")
.identifier("urn:de:fim:leika:leistung:99400048079000")
.build();
final var publicServiceType = new PublicServiceType();
publicServiceType.setName("Test Service");
publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
final var attachment = new Attachment();
attachment.setAttachmentId(attachmentPayload.getAttachmentId());
attachment.setPurpose(Purpose.ATTACHMENT);
attachment.setFilename(attachmentPayload.getFile().getName());
attachment.setMimeType(attachmentPayload.getMimeType());
final var attachmentHash = new Hash();
attachmentHash.setContent(attachmentPayload.getHashedData());
attachmentHash.setSignatureType(SignatureType.SHA_512);
attachment.setHash(attachmentHash);
final PublicServiceType publicServiceType = SubmissionUtil.buildPublicServiceType(serviceType);
final List<Attachment> attachmentMetadata = SubmissionUtil.toAttachmentMetadata(List.of(attachmentPayload));
final Metadata metadata = SubmissionUtil.buildMetadata(attachmentMetadata, data, publicServiceType);
final var contentStructure = new ContentStructure();
contentStructure.setAttachments(List.of(attachment));
contentStructure.setData(data);
final var metadata = new Metadata();
metadata.setSchema(METADATA_V_1_0_0.toString());
metadata.setContentStructure(contentStructure);
metadata.setPublicServiceType(publicServiceType);
final String encryptedMetadata = cryptoService.encryptBytes(encryptionKey, new ObjectMapper().writeValueAsBytes(metadata));
......@@ -170,7 +209,6 @@ class ClientIntegrationTest {
}
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testAbortedSendSubmissionWithKeyValidationNotSilent() {
// Given
......@@ -194,7 +232,6 @@ class ClientIntegrationTest {
class ReceiveSubmissionTests {
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testListSubmissions() {
// Given
......@@ -245,7 +282,6 @@ class ClientIntegrationTest {
class EventTests {
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testRejectEvent() {
// Given
......@@ -276,7 +312,6 @@ class ClientIntegrationTest {
}
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testAcceptEvent() {
// Given
......@@ -307,7 +342,6 @@ class ClientIntegrationTest {
}
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testReadEventLogFromSender() {
// Given
......@@ -316,7 +350,7 @@ class ClientIntegrationTest {
final var submission = SubmissionBuilder.Builder()
.withJsonData("{ \"data\": \"Beispiel Fachdaten\" }")
.withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID")))
.withServiceType("Test Service", "urn:de:fim:leika:")
.withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000")
.build();
final var sentSubmission = ClientFactory.senderClient(config).submit(submission);
......@@ -338,7 +372,6 @@ class ClientIntegrationTest {
}
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testReadEventLogFromSubscriber() {
// Given
......@@ -348,7 +381,7 @@ class ClientIntegrationTest {
.withAttachment(new File("src/test/resources/attachment.txt"))
.withJsonData("{ \"data\": \"Beispiel Fachdaten\" }")
.withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID")))
.withServiceType("Test Service", "urn:de:fim:leika:")
.withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000")
.build();
final var sentSubmission = ClientFactory.senderClient(config).submit(submission);
......@@ -375,7 +408,6 @@ class ClientIntegrationTest {
}
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void testReadSubmissionStatusWithAuthTagEventValidationFromSender() {
// Given
......@@ -387,7 +419,7 @@ class ClientIntegrationTest {
.withAttachment(new File("src/test/resources/attachment.txt"))
.withJsonData("{ \"data\": \"Beispiel Fachdaten\" }")
.withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID")))
.withServiceType("Test Service", "urn:de:fim:leika:")
.withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000")
.build();
final var sentSubmission = ClientFactory.senderClient(config).submit(submission);
......@@ -406,20 +438,104 @@ class ClientIntegrationTest {
}
@Nested
class RoutingTests {
@Test
void testFindDestinationsWithRegionalKey() {
// Given
final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
final RoutingClient routingClient = ClientFactory.routingClient(config);
final DestinationSearch search = DestinationSearch.Builder()
.withLeikaKey("99123456760610")
.withArs("064350014014")
.build();
// When
final List<Route> routes = routingClient.findDestinations(search);
// Then
assertThat(routes, hasSize(1));
assertThat(routes.get(0).getDestinationId(), is(UUID.fromString("d40e7b13-da98-4b09-9e16-bbd61ca81510")));
}
@Test
void testFindDestinationsWithAreaId() {
// Given
final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
final RoutingClient routingClient = ClientFactory.routingClient(config);
final DestinationSearch search = DestinationSearch.Builder()
.withLeikaKey("99123456760610")
.withAreaId("931")
.build();
// When
final List<Route> routes = routingClient.findDestinations(search);
// Then
assertThat(routes, hasSize(1));
assertThat(routes.get(0).getDestinationId(), is(UUID.fromString("d40e7b13-da98-4b09-9e16-bbd61ca81510")));
}
@Test
void testFindDestinationsWithMultipleAreaSearchCriteria() {
// Given
final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
final RoutingClient routingClient = ClientFactory.routingClient(config);
final DestinationSearch search = DestinationSearch.Builder()
.withLeikaKey("99123456760610")
.withArs("064350014014")
.withAreaId("1234")
.build();
// When
final RestApiException exception = assertThrows(RestApiException.class, () -> routingClient.findDestinations(search));
// Then
assertThat(exception.getMessage(), containsString("Only one of ars, ags or areaId must be specified"));
}
@Test
void testFindAreaWithMultipleSearchCriteria() {
// Given
final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
final RoutingClient routingClient = ClientFactory.routingClient(config);
// When
final List<Area> areas = routingClient.findAreas(List.of("Leip*", "04229"), 0, 10);
// Then
assertThat(areas, is(not(empty())));
assertThat(areas.size(), is(lessThanOrEqualTo(10)));
assertTrue(areas.stream().anyMatch(area -> area.getName().equals("Leipzig")));
}
}
@Nested
class AuthenticationTests {
@Test
@EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
void retrieveAuthenticationToken() {
// Given
final var tokenUrl = "https://auth-testing.fit-connect.fitko.dev/token";
final var clientId = "781f6213-0f0f-4a79-9372-e7187ffda98b";
final var secret = "PnzR8Vbmhpv_VwTkT34wponqXWK8WBm-LADlryYdV4o";
final var scope1 = "send:region:DE";
final var scope2 = "send:region:EN";
final var clientId = System.getenv("SUBSCRIBER_CLIENT_ID");
final var secret = System.getenv("SUBSCRIBER_CLIENT_SECRET");
final RestService restService = new RestService(new BuildInfo());
final var authService = new DefaultOAuthService(new RestTemplate(), clientId, secret, tokenUrl);
final var authService = new DefaultOAuthService(restService.getRestTemplate(), clientId, secret, tokenUrl);
// When
final OAuthToken token = authService.getCurrentToken();
......
package dev.fitko.fitconnect.client;
import dev.fitko.fitconnect.api.domain.model.route.Area;
import dev.fitko.fitconnect.api.domain.model.route.AreaResult;
import dev.fitko.fitconnect.api.domain.model.route.Route;
import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.exceptions.RoutingException;
import dev.fitko.fitconnect.api.services.routing.RoutingService;
import dev.fitko.fitconnect.client.router.DestinationSearch;
import dev.fitko.fitconnect.core.routing.RouteVerifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class RoutingClientTest {
RoutingService routingServiceMock;
RouteVerifier routeVerifierMock;
RoutingClient underTest;
@BeforeEach
void setup() {
routingServiceMock = mock(RoutingService.class);
routeVerifierMock = mock(RouteVerifier.class);
underTest = new RoutingClient(routingServiceMock, routeVerifierMock);
}
@Test
void findDestinationsTest() {
// Given
final var firstExpectedRoute = new Route();
firstExpectedRoute.setDestinationId(UUID.randomUUID());
final var secondExpectedRoute = new Route();
secondExpectedRoute.setDestinationId(UUID.randomUUID());
final var expectedRouteResult = new RouteResult();
expectedRouteResult.setRoutes(List.of(firstExpectedRoute, secondExpectedRoute));
when(routingServiceMock.getRoutes(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(expectedRouteResult);
when(routeVerifierMock.validateRouteDestinations(any(), any(), any())).thenReturn(ValidationResult.ok());
final DestinationSearch search = DestinationSearch.Builder()
.withLeikaKey("99400048079000")
.withArs("064350014014")
.withLimit(5)
.build();
// When
final List<Route> destinations = underTest.findDestinations(search);
// Then
assertThat(destinations.size(), is(2));
assertThat(destinations.get(0).getDestinationId(), is(firstExpectedRoute.getDestinationId()));
assertThat(destinations.get(1).getDestinationId(), is(secondExpectedRoute.getDestinationId()));
}
@Test
void findDestinationsWithFailedValidationTest() {
// Given
final var expectedRoute = new Route();
expectedRoute.setDestinationId(UUID.randomUUID());
final var expectedRouteResult = new RouteResult();
expectedRouteResult.setRoutes(List.of(expectedRoute));
when(routingServiceMock.getRoutes(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(expectedRouteResult);
when(routeVerifierMock.validateRouteDestinations(any(), any(), any())).thenReturn(ValidationResult.error(new RoutingException("Route validation failed")));
final DestinationSearch search = DestinationSearch.Builder()
.withLeikaKey("99400048079000")
.withArs("064350014014")
.withLimit(5)
.build();
// When
final RoutingException exception = assertThrows(RoutingException.class, () -> underTest.findDestinations(search));
// Then
assertThat(exception.getMessage(), containsString("Route validation failed"));
}
@Test
void testGetAreasWithSingleSearchExpressionTest() {
// Given
final var expectedArea = new Area();
expectedArea.setName("Leipzig");
final var expectedAreaResult = new AreaResult();
expectedAreaResult.setAreas(List.of(expectedArea));
when(routingServiceMock.getAreas(anyList(), anyInt(), anyInt())).thenReturn(expectedAreaResult);
// When
final List<Area> areas = underTest.findAreas("04229", 0, 5);
// Then
assertThat(areas.size(), is(1));
assertThat(areas.get(0).getName(), is("Leipzig"));
}
@Test
void getAreasWithMultipleFiltersTest() {
// Given
final var expectedArea = new Area();
expectedArea.setName("Leipzig");
final var expectedAreaResult = new AreaResult();
expectedAreaResult.setAreas(List.of(expectedArea));
when(routingServiceMock.getAreas(anyList(), anyInt(), anyInt())).thenReturn(expectedAreaResult);
// When
final List<Area> areas = underTest.findAreas(List.of("Leip*", "04229"), 0, 5);
// Then
assertThat(areas.size(), is(1));
assertThat(areas.get(0).getName(), is("Leipzig"));
}
}
\ No newline at end of file
......@@ -121,7 +121,6 @@ public class SenderClientTest {
when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok());
when(senderMock.validateXmlFormat(any())).thenReturn(ValidationResult.ok());
// When
final var submission = SubmissionBuilder.Builder()
.withAttachment(new File("src/test/resources/attachment.txt"))
......@@ -162,92 +161,19 @@ public class SenderClientTest {
logs.assertContains("Detected attachment mime-type application/pdf");
}
@Test
void testWithMissingData() throws Exception {
// Given
final UUID destinationId = setupTestMocks();
// When
final var submission = SubmissionBuilder.Builder()
.withJsonData(null)
.withDestination(destinationId)
.withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
// Then
assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
}
@Test
void testWithMissingDestinationId() throws Exception {
// Given
setupTestMocks();
// When
final var submission = SubmissionBuilder.Builder()
.withJsonData("{}")
.withDestination(null)
.withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
// Then
assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null"));
}
@Test
void testWithInvalidDestinationId() {
// Given
final var invalidUUID = "1234:132434:5678";
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () ->
SubmissionBuilder.Builder()
.withJsonData("{}")
.withDestination(UUID.fromString(invalidUUID))
.withServiceType("name", "test:key")
.build());
// Then
assertThat(exception.getMessage(), containsString("Invalid UUID string: " + invalidUUID));
}
@Test
void testWithInvalidLeikaKey() throws JOSEException {
// Given
setupTestMocks();
// Given
final var submission = SubmissionBuilder.Builder()
.withJsonData("{\"test\" . \"data\"}")
.withDestination(UUID.randomUUID())
.withServiceType("name", "illegal:test:identifier")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
// Then
assertThat(exception.getMessage(), containsString("LeikaKey has invalid format"));
}
@Test
void testMissingDataMimeTypeSchema() throws Exception {
// Given
final var destinationId = UUID.randomUUID();
final var destinationService = new DestinationService();
destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
final var destination = new Destination();
destination.setServices(Set.of(destinationService));
destination.setDestinationId(destinationId);
final var announcedSubmission = getAnnouncedSubmission(destinationId);
final RSAKey publicKey = generateRsaKey(4096).toPublicJWK();
......@@ -272,68 +198,6 @@ public class SenderClientTest {
logs.assertContains("Required schema to send valid submission not found");
}
@Test
void testWithMissingServiceTypeIdentifier() throws Exception {
// Given
final var destinationId = setupTestMocks();
final var submission = SubmissionBuilder.Builder()
.withJsonData("{}")
.withDestination(destinationId)
.withServiceType("Name", null)
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
// Then
assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null"));
}
@Test
void testMissingEncryptedMetadata() throws Exception {
// Given
final var destinationId = setupTestMocks();
// When
final EncryptedSubmissionPayload submission = EncryptedSubmissionBuilder.Builder()
.withEncryptedData("encr$&ted d/&)ata")
.withEncryptedMetadata(null)
.withDestination(destinationId)
.withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
// Then
assertThat(exception.getMessage(), containsString("Encrypted metadata must not be null"));
}
@Test
void testMissingEncryptedData() throws Exception {
// Given
final var destinationId = setupTestMocks();
// When
final EncryptedSubmissionPayload submission = EncryptedSubmissionBuilder.Builder()
.withEncryptedData(null)
.withEncryptedMetadata("encr$&ted met@d/&)ata")
.withDestination(destinationId)
.withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
// Then
assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
}
@Test
void testWithEmptyAttachments() throws Exception {
......@@ -445,7 +309,7 @@ public class SenderClientTest {
// Given
final var destinationId = setupTestMocks();
when(senderMock.getDestination(any())).thenThrow(new RestApiException("Loading destination failed"));
when(senderMock.createSubmission(any())).thenThrow(new RestApiException("Announcing submission failed"));
// When
final SubmissionPayload submission = SubmissionBuilder.Builder()
......@@ -635,6 +499,7 @@ public class SenderClientTest {
}
private UUID setupTestMocks() throws JOSEException {
final var destinationId = UUID.randomUUID();
final var destination = getDestination(destinationId);
final var announcedSubmission = getAnnouncedSubmission(destinationId);
......@@ -674,6 +539,7 @@ public class SenderClientTest {
schema.setMimeType(MimeType.APPLICATION_JSON);
final var destinationService = new DestinationService();
destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
destinationService.setSubmissionSchemas(Set.of(schema));
final var destination = new Destination();
......@@ -684,6 +550,7 @@ public class SenderClientTest {
private Destination getDestination(final UUID destinationId, final SubmissionSchema schema) {
final var destinationService = new DestinationService();
destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
destinationService.setSubmissionSchemas(Set.of(schema));
final var destination = new Destination();
......
......@@ -16,6 +16,7 @@ class ClientFactoryTest {
void testMissingConfiguration() {
assertThrows(InitializationException.class, ClientFactory::senderClient);
assertThrows(InitializationException.class, ClientFactory::subscriberClient);
assertThrows(InitializationException.class, ClientFactory::routingClient);
}
@Test
......@@ -61,6 +62,23 @@ class ClientFactoryTest {
assertNotNull(ClientFactory.subscriberClient(subscriberConfig));
}
@Test
void testRoutingClientConstruction() {
final var envName = new EnvironmentName("DEV");
final var environment = new Environment("", "https://routing.fitko.fitconnect.de", "", "", true);
final var senderConfig = new SenderConfig("1234", "abcd");
final var routingConfig = ApplicationConfig.builder()
.environments(Map.of(envName, environment))
.activeEnvironment(envName)
.senderConfig(senderConfig)
.build();
assertNotNull(ClientFactory.routingClient(routingConfig));
}
@Test
void testSigningKeyCannotBeParsed() {
......
......@@ -6,7 +6,6 @@ import ch.qos.logback.core.AppenderBase;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public final class LogCaptor extends AppenderBase<LoggingEvent> {
......
package dev.fitko.fitconnect.client.util;
import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType;
import dev.fitko.fitconnect.api.domain.model.metadata.Signature;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
import org.junit.jupiter.api.Test;
import java.util.List;
import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class SubmissionUtilTest {
@Test
public void buildMetadata() {
Signature signature = new Signature();
signature.setContent("content");
Data data = new Data();
data.setSignature(signature);
Attachment attachment1 = new Attachment();
attachment1.setFilename("fileName1");
Attachment attachment2 = new Attachment();
attachment2.setFilename("fileName2");
List<Attachment> attachments = List.of(attachment1, attachment2);
PublicServiceType publicServiceType = new PublicServiceType();
publicServiceType.setName("publicService");
Metadata metadata = SubmissionUtil.buildMetadata(attachments, data, publicServiceType);
assertThat(metadata.getSchema(), is(METADATA_V_1_0_0.toString()));
assertThat(metadata.getContentStructure().getAttachments().size(), is(2));
assertThat(metadata.getContentStructure().getAttachments().get(0).getFilename(), is("fileName1"));
assertThat(metadata.getContentStructure().getAttachments().get(1).getFilename(), is("fileName2"));
assertThat(metadata.getContentStructure().getData().getSignature().getContent(), is("content"));
assertThat(metadata.getPublicServiceType().getName(), is("publicService"));
}
}
package dev.fitko.fitconnect.client.util;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import dev.fitko.fitconnect.api.services.Sender;
import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder;
import dev.fitko.fitconnect.client.sender.SubmissionBuilder;
import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload;
import dev.fitko.fitconnect.client.sender.model.SubmissionPayload;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.Set;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class ValidDataGuardTest {
ValidDataGuard underTest;
Sender senderMock;
@BeforeEach
void setUp() {
senderMock = mock(Sender.class);
underTest = new ValidDataGuard(senderMock);
}
@Nested
class SubmissionPayloadDataTests {
@Test
void testValidSubmissionPayload() {
// Given
final DestinationService service = new DestinationService();
service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
final Destination destination = new Destination();
destination.setDestinationId(UUID.randomUUID());
destination.setServices(Set.of(service));
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
.withJsonData("\"test\": \"json\"")
.withDestination(UUID.randomUUID())
.withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
.build();
// Then valid if no exception is thrown
underTest.ensureValidDataPayload(submissionPayload);
}
@Test
void testMissingJsonData() {
// Given
final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
.withJsonData(null)
.withDestination(UUID.randomUUID())
.withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
}
@Test
void testMissingXmlData() {
// Given
final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
.withXmlData(null)
.withDestination(UUID.randomUUID())
.withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
}
@Test
void testMissingDestinationId() {
// Given
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
.withJsonData("{}")
.withDestination(null)
.withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
// Then
assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null"));
}
@Test
void testInvalidServiceIdentifier() {
// Given
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
.withJsonData("{\"test\" . \"data\"}")
.withDestination(UUID.randomUUID())
.withServiceType("name", "illegal:test:identifier")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
// Then
assertThat(exception.getMessage(), containsString("LeikaKey has invalid format"));
}
@Test
void testMissingServiceIdentifier() {
// Given
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
.withJsonData("{\"test\" . \"data\"}")
.withDestination(UUID.randomUUID())
.withServiceType("name", null)
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null"));
}
@Test
void testServiceIdentifierNotMatchingDestination() {
// Given
final DestinationService service = new DestinationService();
service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
final Destination destination = new Destination();
destination.setServices(Set.of(service));
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
.withJsonData("\"test\": \"json\"")
.withDestination(UUID.randomUUID())
.withServiceType("Test", "urn:de:fim:leika:leistung:123456789101114")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Provided service type 'urn:de:fim:leika:leistung:123456789101114' is not allowed by the destination"));
}
}
@Nested
class EncryptedSubmissionPayloadDataTests {
@Test
void testValidEncryptedSubmissionPayload() {
// Given
final DestinationService service = new DestinationService();
service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
final Destination destination = new Destination();
destination.setDestinationId(UUID.randomUUID());
destination.setServices(Set.of(service));
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
.withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withDestination(UUID.randomUUID())
.withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
.build();
// Then valid if no exception is thrown
underTest.ensureValidDataPayload(encryptedSubmissionPayload);
}
@Test
void testMissingEncryptedData() {
// Given
final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
.withEncryptedData(null)
.withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withDestination(UUID.randomUUID())
.withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Encrypted data is mandatory, but was null"));
}
@Test
void testMissingEncryptedMetadata() {
// Given
final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
.withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withEncryptedMetadata(null)
.withDestination(UUID.randomUUID())
.withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Encrypted metadata must not be null"));
}
@Test
void testMissingDestinationId() {
// Given
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
.withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withDestination(null)
.withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
// Then
assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null"));
}
@Test
void testInvalidServiceIdentifier() {
// Given
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
.withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withDestination(UUID.randomUUID())
.withServiceType("name", "illegal:test:identifier")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
// Then
assertThat(exception.getMessage(), containsString("LeikaKey has invalid format"));
}
@Test
void testMissingServiceIdentifier() {
// Given
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
.withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withDestination(UUID.randomUUID())
.withServiceType("name", null)
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null"));
}
@Test
void testServiceIdentifierNotMatchingDestination() {
// Given
final DestinationService service = new DestinationService();
service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
final Destination destination = new Destination();
destination.setServices(Set.of(service));
when(senderMock.getDestination(any())).thenReturn(destination);
when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
.withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
.withDestination(UUID.randomUUID())
.withServiceType("name", "urn:de:fim:leika:leistung:11111111111111")
.build();
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
// Then
assertThat(exception.getMessage(), containsString("Provided service type 'urn:de:fim:leika:leistung:11111111111111' is not allowed by the destination"));
}
}
@Nested
class MiscTests {
@Test
void testInvalidDestinationId() {
// Given
final var invalidUUID = "1234:132434:5678";
// When
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () ->
SubmissionBuilder.Builder()
.withJsonData("{}")
.withDestination(UUID.fromString(invalidUUID))
.withServiceType("name", "test:key")
.build());
// Then
assertThat(exception.getMessage(), containsString("Invalid UUID string: " + invalidUUID));
}
}
}
\ No newline at end of file
......@@ -60,8 +60,9 @@ public class SubmissionSender implements Sender {
}
@Override
public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
return this.validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
LOGGER.info("Validating callback integrity");
return validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
}
@Override
......