diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayRevocationListDownloadConnector.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayRevocationListDownloadConnector.java new file mode 100644 index 0000000..81ce665 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/DgcGatewayRevocationListDownloadConnector.java @@ -0,0 +1,132 @@ +package eu.europa.ec.dgc.gateway.connector; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.dto.RevocationBatchDto; +import eu.europa.ec.dgc.gateway.connector.exception.RevocationBatchDownloadException; +import eu.europa.ec.dgc.gateway.connector.exception.RevocationBatchGoneException; +import eu.europa.ec.dgc.gateway.connector.exception.RevocationBatchParseException; +import eu.europa.ec.dgc.gateway.connector.iterator.DgcGatewayRevocationListDownloadIterator; +import eu.europa.ec.dgc.signing.SignedMessageParser; +import eu.europa.ec.dgc.signing.SignedStringMessageParser; +import feign.FeignException; +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Scope; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@ConditionalOnProperty("dgc.gateway.connector.enabled") +@Service +@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) +@RequiredArgsConstructor +@Slf4j +public class DgcGatewayRevocationListDownloadConnector { + + private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient; + private final ObjectMapper objectMapper; + + /** + * Gets a revocation list iterator, for partly downloading the revocation list. + * The if-modified-since header is set to the default value to start at the beginning of the list. + * @return revocation list iterator + */ + public DgcGatewayRevocationListDownloadIterator getRevocationListDownloadIterator() { + return new DgcGatewayRevocationListDownloadIterator(dgcGatewayConnectorRestClient); + } + + /** + * Gets a revocation list iterator, for partly downloading the revocation list. + * The if-modified-since header is set to the value of the parameter. Only newer part of the list are downloaded. + * @param ifModifiedSinceDate The value for the if-modified-since header + * @return revocation list iterator + */ + public DgcGatewayRevocationListDownloadIterator getRevocationListDownloadIterator( + ZonedDateTime ifModifiedSinceDate) { + + return new DgcGatewayRevocationListDownloadIterator(dgcGatewayConnectorRestClient, ifModifiedSinceDate); + } + + /** + * Gets the revocation list batch data for a given batchId. + * @param batchId the id of the batch to download. + * @return the batch data. + */ + public RevocationBatchDto getRevocationListBatchById(String batchId) throws RevocationBatchDownloadException, + RevocationBatchGoneException, RevocationBatchParseException { + + ResponseEntity responseEntity; + + try { + responseEntity = dgcGatewayConnectorRestClient.downloadBatch(batchId); + } catch (FeignException e) { + log.error("Download of revocation list batch failed. DGCG responded with status code: {}", e.status()); + + if (e.status() == HttpStatus.GONE.value()) { + throw new RevocationBatchGoneException(String.format("Batch already gone: %s", batchId),batchId); + } + + throw new RevocationBatchDownloadException("Batch download failed with exception.", e); + } + + if (responseEntity.getStatusCode() != HttpStatus.OK) { + int statusCode = responseEntity.getStatusCode().value(); + log.error("Download of revocation list batch failed. DGCG responded with status code: {}", statusCode); + + throw new RevocationBatchDownloadException( + String.format("Batch download failed with unexpected response. Response status code: %d", statusCode), + statusCode); + } + + String cms = responseEntity.getBody(); + + if (!checkCmsSignature(cms)) { + log.error("CMS check failed for revocation batch: {}", batchId); + throw new RevocationBatchParseException( + String.format("CMS check failed for revocation batch: %s", batchId), batchId); + } + + return map(cms, batchId); + } + + private boolean checkCmsSignature(String cms) { + SignedStringMessageParser parser = + new SignedStringMessageParser(cms); + + if (parser.getParserState() != SignedMessageParser.ParserState.SUCCESS) { + log.error("Invalid CMS for Revocation List Batch."); + return false; + } + + if (!parser.isSignatureVerified()) { + log.error("Invalid CMS Signature for Revocation List Batch"); + return false; + } + + return true; + } + + private RevocationBatchDto map(String cms, String batchId) { + SignedStringMessageParser parser = + new SignedStringMessageParser(cms); + + try { + objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true); + return objectMapper.readValue(parser.getPayload(), RevocationBatchDto.class); + } catch (JsonProcessingException e) { + log.error("Failed to parse revocation batch JSON: {}", e.getMessage()); + + throw new RevocationBatchParseException( + String.format("Failed to parse revocation batch JSON: %s", e.getMessage()), batchId); + } + + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java index c4423e5..a673b64 100644 --- a/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/client/DgcGatewayConnectorRestClient.java @@ -21,12 +21,14 @@ package eu.europa.ec.dgc.gateway.connector.client; import eu.europa.ec.dgc.gateway.connector.dto.CertificateTypeDto; +import eu.europa.ec.dgc.gateway.connector.dto.RevocationBatchListDto; import eu.europa.ec.dgc.gateway.connector.dto.TrustListItemDto; import eu.europa.ec.dgc.gateway.connector.dto.ValidationRuleDto; import java.util.List; import java.util.Map; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -34,6 +36,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; @ConditionalOnProperty("dgc.gateway.connector.enabled") @FeignClient( @@ -119,4 +122,22 @@ public interface DgcGatewayConnectorRestClient { @GetMapping(value = "/rules/{cc}", produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity>> downloadValidationRule(@PathVariable("cc") String countryCode); + + /** + * Downloads a batch list from the revocation list. + * + */ + @GetMapping(value = "/revocation-list", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity downloadRevocationList( + @RequestHeader(HttpHeaders.IF_MODIFIED_SINCE) String lastUpdate); + + /** + * Downloads a batch of the revocation list. + * + * @param batchId ID of the batch to download + * @return batch as cms massage + */ + @GetMapping(value = "/revocation-list/{batchId}", produces = {"application/cms-text"}) + ResponseEntity downloadBatch(@PathVariable("batchId") String batchId); + } diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationBatchDto.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationBatchDto.java new file mode 100644 index 0000000..096537e --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationBatchDto.java @@ -0,0 +1,53 @@ +/*- + * ---license-start + * eu-digital-green-certificates / dgc-lib + * --- + * Copyright (C) 2022 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.dto; + +import java.time.ZonedDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RevocationBatchDto { + + private String country; + + private ZonedDateTime expires; + + private String kid; + + private RevocationHashTypeDto hashType; + + private List entries; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class BatchEntryDto { + + private String hash; + + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationBatchListDto.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationBatchListDto.java new file mode 100644 index 0000000..00a9d08 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationBatchListDto.java @@ -0,0 +1,51 @@ +/*- + * ---license-start + * eu-digital-green-certificates / dgc-lib + * --- + * Copyright (C) 2022 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.dto; + +import java.time.ZonedDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +public class RevocationBatchListDto { + + + private Boolean more; + + private List batches; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class RevocationBatchListItemDto { + + private String batchId; + + private String country; + + private ZonedDateTime date; + + private Boolean deleted; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationHashTypeDto.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationHashTypeDto.java new file mode 100644 index 0000000..c28e52b --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/dto/RevocationHashTypeDto.java @@ -0,0 +1,36 @@ +/*- + * ---license-start + * eu-digital-green-certificates / dgc-lib + * --- + * Copyright (C) 2022 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.dto; + +// Type of hash for revocation lists +public enum RevocationHashTypeDto { + + // The hash is calculated over the UCI string encoded in UTF-8 and converted to a byte array. + UCI, + + // The hash is calculated over the bytes of the COSE_SIGN1 signature from the CWT + SIGNATURE, + + // The CountryCode encoded as a UTF-8 string concatenated with the UCI encoded with a + // UTF-8 string. This is then converted to a byte array and used as input to the hash function.") + COUNTRYCODEUCI + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchDownloadException.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchDownloadException.java new file mode 100644 index 0000000..61ea591 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchDownloadException.java @@ -0,0 +1,30 @@ +package eu.europa.ec.dgc.gateway.connector.exception; + +import lombok.Getter; + +@Getter +public class RevocationBatchDownloadException extends RuntimeException { + + + private final int status; + + public RevocationBatchDownloadException(String message, Throwable inner) { + super(message, inner); + this.status = 500; + } + + public RevocationBatchDownloadException(String message) { + super(message); + this.status = 500; + } + + public RevocationBatchDownloadException(String message, Throwable inner, int status) { + super(message, inner); + this.status = status; + } + + public RevocationBatchDownloadException(String message, int status) { + super(message); + this.status = status; + } +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchGoneException.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchGoneException.java new file mode 100644 index 0000000..ce7d50b --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchGoneException.java @@ -0,0 +1,15 @@ +package eu.europa.ec.dgc.gateway.connector.exception; + +import lombok.Getter; + +@Getter +public class RevocationBatchGoneException extends RuntimeException { + + private final String batchId; + + public RevocationBatchGoneException(String message, String batchId) { + super(message); + this.batchId = batchId; + } + +} diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchParseException.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchParseException.java new file mode 100644 index 0000000..5027a26 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/exception/RevocationBatchParseException.java @@ -0,0 +1,16 @@ +package eu.europa.ec.dgc.gateway.connector.exception; + +import lombok.Getter; + +@Getter +public class RevocationBatchParseException extends RuntimeException { + + private final String batchId; + + public RevocationBatchParseException(String message, String batchId) { + super(message); + this.batchId = batchId; + } + +} + diff --git a/src/main/java/eu/europa/ec/dgc/gateway/connector/iterator/DgcGatewayRevocationListDownloadIterator.java b/src/main/java/eu/europa/ec/dgc/gateway/connector/iterator/DgcGatewayRevocationListDownloadIterator.java new file mode 100644 index 0000000..5099ae0 --- /dev/null +++ b/src/main/java/eu/europa/ec/dgc/gateway/connector/iterator/DgcGatewayRevocationListDownloadIterator.java @@ -0,0 +1,153 @@ +/*- + * ---license-start + * eu-digital-green-certificates / dgc-lib + * --- + * Copyright (C) 2022 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package eu.europa.ec.dgc.gateway.connector.iterator; + +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.dto.RevocationBatchListDto; +import eu.europa.ec.dgc.gateway.connector.dto.RevocationBatchListDto.RevocationBatchListItemDto; +import feign.FeignException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + + +/** + * This class provides an Iterator for downloading the revocation List parts from the gateway. + */ + +@ConditionalOnProperty("dgc.gateway.connector.enabled") +@Slf4j +public class DgcGatewayRevocationListDownloadIterator implements Iterator> { + + private final DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient; + + private final DateTimeFormatter dateFormat; + + private ZonedDateTime lastUpdated = null; + + private List nextData; + + private boolean hasNext = false; + + /** + * Creates a new Iterator instance for downloading the revocation list from the dgc gateway. + * The If-Modified-Since Header is set to the default value and the download should start with the first + * part of the revocation list. + * + * @param dgcGatewayConnectorRestClient The rest client for the connection to the dgc gateway + */ + + public DgcGatewayRevocationListDownloadIterator(DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient) { + this(dgcGatewayConnectorRestClient, ZonedDateTime.parse("2021-06-01T00:00:00Z")); + } + + /** + * Creates a new Iterator instance for downloading the revocation list from the dgc gateway. + * The If-Modified-Since Header is set to the given value and only newer parts of the revocation list + * are downloaded. + * + * @param dgcGatewayConnectorRestClient The rest client for the connection to the dgc gateway + * @param ifModifiedSinceDate The value for the If-Modified-Since date + */ + + public DgcGatewayRevocationListDownloadIterator(DgcGatewayConnectorRestClient dgcGatewayConnectorRestClient, + ZonedDateTime ifModifiedSinceDate) { + this.dgcGatewayConnectorRestClient = dgcGatewayConnectorRestClient; + dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); + setIfModifiedSinceDate(ifModifiedSinceDate); + } + + /** + * Sets the If-Modified-Since date and downloads the next newer part of the revocation list from the dgc gateway. + * + * @param dateTime The value for the If-Modified-Since date + */ + public void setIfModifiedSinceDate(ZonedDateTime dateTime) { + lastUpdated = dateTime; + fetchNextRevocationListPart(); + } + + @Override + public boolean hasNext() { + return hasNext; + } + + @Override + public List next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + List returnData = nextData; + fetchNextRevocationListPart(); + return returnData; + } + + protected String toIsoO8601(ZonedDateTime dateTime) { + return dateTime.format(dateFormat); + } + + private void fetchNextRevocationListPart() { + log.info("Fetching Revocation List from DGCG with If-Modified-Since date: {}", toIsoO8601(lastUpdated)); + + ResponseEntity responseEntity; + + hasNext = false; + nextData = null; + + try { + responseEntity = dgcGatewayConnectorRestClient.downloadRevocationList(toIsoO8601(lastUpdated)); + } catch (FeignException e) { + log.error("Download of revocation list failed. DGCG responded with status code: {}", + e.status()); + return; + } + + if (responseEntity.getStatusCode() != HttpStatus.OK + && responseEntity.getStatusCode() != HttpStatus.NO_CONTENT) { + + log.error("DGCG responded with unexpected status code: {}", + responseEntity.getStatusCode()); + return; + } + + RevocationBatchListDto downloadedBatchList = responseEntity.getBody(); + + if (responseEntity.getStatusCode() == HttpStatus.NO_CONTENT + || downloadedBatchList == null) { + log.debug("No Content received for download with If-Modified-Since date: {}", toIsoO8601(lastUpdated)); + } else { + + if (downloadedBatchList.getBatches().isEmpty()) { + log.debug("No Content received for download with If-Modified-Since date: {}", toIsoO8601(lastUpdated)); + } else { + nextData = downloadedBatchList.getBatches(); + hasNext = true; + lastUpdated = nextData.get(nextData.size() - 1).getDate(); + } + } + } +} diff --git a/src/test/java/eu/europa/ec/dgc/gateway/connector/RevocationListDownloadConnectorTest.java b/src/test/java/eu/europa/ec/dgc/gateway/connector/RevocationListDownloadConnectorTest.java new file mode 100644 index 0000000..63e6362 --- /dev/null +++ b/src/test/java/eu/europa/ec/dgc/gateway/connector/RevocationListDownloadConnectorTest.java @@ -0,0 +1,312 @@ +package eu.europa.ec.dgc.gateway.connector; + +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.europa.ec.dgc.gateway.connector.client.DgcGatewayConnectorRestClient; +import eu.europa.ec.dgc.gateway.connector.dto.RevocationBatchDto; +import eu.europa.ec.dgc.gateway.connector.dto.RevocationBatchListDto; +import eu.europa.ec.dgc.gateway.connector.dto.RevocationHashTypeDto; +import eu.europa.ec.dgc.gateway.connector.exception.RevocationBatchDownloadException; +import eu.europa.ec.dgc.gateway.connector.exception.RevocationBatchParseException; +import eu.europa.ec.dgc.gateway.connector.iterator.DgcGatewayRevocationListDownloadIterator; +import eu.europa.ec.dgc.signing.SignedStringMessageBuilder; +import eu.europa.ec.dgc.testdata.CertificateTestUtils; +import eu.europa.ec.dgc.testdata.DgcTestKeyStore; +import eu.europa.ec.dgc.utils.CertificateUtils; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@SpringBootTest +@Slf4j +class RevocationListDownloadConnectorTest { + + @MockBean + DgcGatewayConnectorRestClient restClientMock; + + @Autowired + DgcGatewayRevocationListDownloadConnector downloadConnector; + + @Autowired + CertificateUtils certificateUtils; + + @Autowired + DgcTestKeyStore testKeyStore; + + @Autowired + ObjectMapper objectMapper; + + + + @Test + void getRevocationListDownloadIterator() { + RevocationBatchListDto.RevocationBatchListItemDto batchItem1 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId1", + "de", + ZonedDateTime.parse("2021-07-01T00:00:00Z"), + false + ); + + RevocationBatchListDto.RevocationBatchListItemDto batchItem2 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId2", + "fr", + ZonedDateTime.parse("2021-08-01T00:00:00Z"), + false + ); + + RevocationBatchListDto.RevocationBatchListItemDto batchItem3 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId3", + "de", + ZonedDateTime.parse("2021-08-01T00:00:00Z"), + false + ); + + RevocationBatchListDto.RevocationBatchListItemDto batchItem4 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId4", + "fr", + ZonedDateTime.parse("2021-08-10T00:00:00Z"), + false + ); + + RevocationBatchListDto responseBody1 = new RevocationBatchListDto(); + responseBody1.setMore(true); + responseBody1.setBatches(new ArrayList<>()); + responseBody1.getBatches().add(batchItem1); + responseBody1.getBatches().add(batchItem2); + + RevocationBatchListDto responseBody2 = new RevocationBatchListDto(); + responseBody2.setMore(false); + responseBody2.setBatches(new ArrayList<>()); + responseBody2.getBatches().add(batchItem3); + responseBody2.getBatches().add(batchItem4); + + + when(restClientMock.downloadRevocationList("2021-06-01T00:00:00Z")) + .thenReturn(ResponseEntity.ok(responseBody1)); + + when(restClientMock.downloadRevocationList("2021-08-01T00:00:00Z")) + .thenReturn(ResponseEntity.ok(responseBody2)); + + when(restClientMock.downloadRevocationList("2021-08-10T00:00:00Z")) + .thenReturn(ResponseEntity.noContent().build()); + + DgcGatewayRevocationListDownloadIterator downloadIterator = + downloadConnector.getRevocationListDownloadIterator(); + + assertTrue(downloadIterator.hasNext()); + List downloadedData = downloadIterator.next(); + + assertNotNull(downloadedData); + assertFalse(downloadedData.isEmpty()); + Assertions.assertEquals(2 , downloadedData.size()); + assertEquals(batchItem1, downloadedData.get(0)); + assertEquals(batchItem2, downloadedData.get(1)); + + assertTrue(downloadIterator.hasNext()); + downloadedData = downloadIterator.next(); + + assertNotNull(downloadedData); + assertFalse(downloadedData.isEmpty()); + Assertions.assertEquals(2 , downloadedData.size()); + assertEquals(batchItem3, downloadedData.get(0)); + assertEquals(batchItem4, downloadedData.get(1)); + + assertFalse(downloadIterator.hasNext()); + + } + + @Test + void GetRevocationListDownloadIteratorWithStartDate() { + RevocationBatchListDto.RevocationBatchListItemDto batchItem1 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId1", + "de", + ZonedDateTime.parse("2021-07-01T00:00:00Z"), + false + ); + + RevocationBatchListDto.RevocationBatchListItemDto batchItem2 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId2", + "fr", + ZonedDateTime.parse("2021-08-01T00:00:00Z"), + false + ); + + RevocationBatchListDto.RevocationBatchListItemDto batchItem3 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId3", + "de", + ZonedDateTime.parse("2021-08-01T00:00:00Z"), + false + ); + + RevocationBatchListDto.RevocationBatchListItemDto batchItem4 = + new RevocationBatchListDto.RevocationBatchListItemDto( + "batchId4", + "fr", + ZonedDateTime.parse("2021-08-10T00:00:00Z"), + false + ); + + RevocationBatchListDto responseBody1 = new RevocationBatchListDto(); + responseBody1.setMore(true); + responseBody1.setBatches(new ArrayList<>()); + responseBody1.getBatches().add(batchItem1); + responseBody1.getBatches().add(batchItem2); + + RevocationBatchListDto responseBody2 = new RevocationBatchListDto(); + responseBody2.setMore(false); + responseBody2.setBatches(new ArrayList<>()); + responseBody2.getBatches().add(batchItem3); + responseBody2.getBatches().add(batchItem4); + + + when(restClientMock.downloadRevocationList("2021-06-01T00:00:00Z")) + .thenReturn(ResponseEntity.ok(responseBody1)); + + when(restClientMock.downloadRevocationList("2021-08-01T00:00:00Z")) + .thenReturn(ResponseEntity.ok(responseBody2)); + + when(restClientMock.downloadRevocationList("2021-08-10T00:00:00Z")) + .thenReturn(ResponseEntity.noContent().build()); + + DgcGatewayRevocationListDownloadIterator downloadIterator = + downloadConnector.getRevocationListDownloadIterator(ZonedDateTime.parse("2021-08-01T00:00:00Z")); + + assertTrue(downloadIterator.hasNext()); + List downloadedData = downloadIterator.next(); + + assertNotNull(downloadedData); + assertFalse(downloadedData.isEmpty()); + Assertions.assertEquals(2 , downloadedData.size()); + assertEquals(batchItem3, downloadedData.get(0)); + assertEquals(batchItem4, downloadedData.get(1)); + + assertFalse(downloadIterator.hasNext()); + } + + @Test + void getRevocationListBatchById() throws Exception { + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + RevocationBatchDto batch = new RevocationBatchDto("de", ZonedDateTime.now().plusDays(1), + "UNKOWN_KID", + RevocationHashTypeDto.SIGNATURE, + List.of(new RevocationBatchDto.BatchEntryDto("cafe"))); + + String batchId1 = "batchId1"; + + String signedBatch = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(objectMapper.writeValueAsString(batch)) + .buildAsString(false); + + + when(restClientMock.downloadBatch(batchId1)) + .thenReturn(ResponseEntity.ok(signedBatch)); + + RevocationBatchDto downloadedBatch = downloadConnector.getRevocationListBatchById(batchId1); + + assertNotNull(downloadedBatch); + assertEquals(downloadedBatch, batch); + + } + + @Test + void getRevocationListBatchByIdNotFoundBatch() throws Exception { + String batchId1 = "batchId1"; + when(restClientMock.downloadBatch(batchId1)) + .thenReturn(ResponseEntity.status(HttpStatus.NOT_FOUND.value()).build()); + + RevocationBatchDownloadException exception = assertThrows(RevocationBatchDownloadException.class, () -> { + downloadConnector.getRevocationListBatchById(batchId1); + }); + + Assertions.assertEquals(exception.getStatus(), HttpStatus.NOT_FOUND.value()); + } + + @Test + void getRevocationListBatchByIdCMSJsonFail() throws Exception { + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String batch = "NoValidJSON"; + + String batchId1 = "batchId1"; + + String signedBatch = new SignedStringMessageBuilder() + .withSigningCertificate(certificateUtils.convertCertificate(upload), keyPairUpload.getPrivate()) + .withPayload(batch) + .buildAsString(false); + + + when(restClientMock.downloadBatch(batchId1)) + .thenReturn(ResponseEntity.ok(signedBatch)); + + RevocationBatchParseException exception = assertThrows(RevocationBatchParseException.class, () -> { + downloadConnector.getRevocationListBatchById(batchId1); + }); + assertTrue(exception.getMessage().contains("Failed to parse revocation batch JSON")); + + } + + @Test + void getRevocationListBatchByIdCMSJFail() throws Exception { + KeyPair keyPairUpload = KeyPairGenerator.getInstance("ec").generateKeyPair(); + X509Certificate upload = CertificateTestUtils.generateCertificate(keyPairUpload, "EU", "UPLOAD"); + + String batch = "NoValidCMS"; + + String batchId1 = "batchId1"; + + when(restClientMock.downloadBatch(batchId1)) + .thenReturn(ResponseEntity.ok(batch)); + + RevocationBatchParseException exception = assertThrows(RevocationBatchParseException.class, () -> { + downloadConnector.getRevocationListBatchById(batchId1); + }); + assertTrue(exception.getMessage().contains("CMS check failed for revocation batch")); + + } + + private void assertEquals(RevocationBatchListDto.RevocationBatchListItemDto b1, + RevocationBatchListDto.RevocationBatchListItemDto b2) { + Assertions.assertEquals(b1.getBatchId(), b2.getBatchId()); + Assertions.assertEquals(b1.getCountry(), b2.getCountry()); + Assertions.assertEquals(b1.getDate(), b2.getDate()); + Assertions.assertEquals(b1.getDeleted(), b2.getDeleted()); + } + + private void assertEquals(RevocationBatchDto b1, RevocationBatchDto b2) { + Assertions.assertEquals(b1.getCountry(), b2.getCountry()); + Assertions.assertTrue(b1.getExpires().isEqual(b2.getExpires())); + Assertions.assertEquals(b1.getHashType(), b2.getHashType()); + Assertions.assertEquals(b1.getKid(), b2.getKid()); + Assertions.assertEquals(b1.getEntries().size(), b2.getEntries().size()); + for (int i = 0; i < b1.getEntries().size(); i++){ + Assertions.assertEquals(b1.getEntries().get(i).getHash(), b2.getEntries().get(i).getHash()); + } + } +} \ No newline at end of file