diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/pom.xml b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/pom.xml
index 9bafb6d9..ccfbe783 100644
--- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/pom.xml
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/pom.xml
@@ -94,7 +94,19 @@
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ Jar Tests Package
+ package
+
+ test-jar
+
+
+
+
-
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/data/VerifierDataServiceTest.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/data/VerifierDataServiceTest.java
index df9ed047..086130d4 100644
--- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/data/VerifierDataServiceTest.java
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/data/VerifierDataServiceTest.java
@@ -10,15 +10,16 @@
package ch.admin.bag.covidcertificate.backend.verifier.data;
+import static ch.admin.bag.covidcertificate.backend.verifier.data.util.TestUtil.getDefaultCsca;
+import static ch.admin.bag.covidcertificate.backend.verifier.data.util.TestUtil.getEcDsc;
+import static ch.admin.bag.covidcertificate.backend.verifier.data.util.TestUtil.getRsaDsc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import ch.admin.bag.covidcertificate.backend.verifier.model.CertSource;
-import ch.admin.bag.covidcertificate.backend.verifier.model.cert.Algorithm;
import ch.admin.bag.covidcertificate.backend.verifier.model.cert.CertFormat;
import ch.admin.bag.covidcertificate.backend.verifier.model.cert.ClientCert;
-import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.DbCsca;
import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.DbDsc;
import java.util.Collections;
import java.util.List;
@@ -208,41 +209,4 @@ void findMaxDscsTest() {
assertTrue(verifierDataService.findDscs(maxDscPkId, CertFormat.IOS, null).isEmpty());
assertEquals(1, verifierDataService.findDscs(maxDscPkId - 1, CertFormat.IOS, null).size());
}
-
- private DbCsca getDefaultCsca(int idSuffix, String origin) {
- var dbCsca = new DbCsca();
- dbCsca.setKeyId("keyid_" + idSuffix);
- dbCsca.setCertificateRaw("cert");
- dbCsca.setOrigin(origin);
- dbCsca.setSubjectPrincipalName("admin_ch");
- return dbCsca;
- }
-
- private DbDsc getRsaDsc(int idSuffix, String origin, long fkCsca) {
- final var dbDsc = new DbDsc();
- dbDsc.setKeyId("keyid_" + idSuffix);
- dbDsc.setFkCsca(fkCsca);
- dbDsc.setCertificateRaw("cert");
- dbDsc.setOrigin(origin);
- dbDsc.setUse("sig");
- dbDsc.setAlg(Algorithm.RS256);
- dbDsc.setN("n");
- dbDsc.setE("e");
- dbDsc.setSubjectPublicKeyInfo("pk");
- return dbDsc;
- }
-
- private DbDsc getEcDsc(int idSuffix, String origin, long fkCsca) {
- final var dbDsc = new DbDsc();
- dbDsc.setKeyId("keyid_" + idSuffix);
- dbDsc.setFkCsca(fkCsca);
- dbDsc.setCertificateRaw("cert");
- dbDsc.setOrigin(origin);
- dbDsc.setUse("sig");
- dbDsc.setAlg(Algorithm.ES256);
- dbDsc.setCrv("crv");
- dbDsc.setX("x");
- dbDsc.setY("y");
- return dbDsc;
- }
}
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/data/util/TestUtil.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/data/util/TestUtil.java
new file mode 100644
index 00000000..1f2331a2
--- /dev/null
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/data/util/TestUtil.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2021 Ubique Innovation AG
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package ch.admin.bag.covidcertificate.backend.verifier.data.util;
+
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.Algorithm;
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.DbCsca;
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.DbDsc;
+
+public class TestUtil {
+
+ private TestUtil() {}
+
+ public static DbCsca getDefaultCsca(int idSuffix, String origin) {
+ var dbCsca = new DbCsca();
+ dbCsca.setKeyId("keyid_" + idSuffix);
+ dbCsca.setCertificateRaw("cert");
+ dbCsca.setOrigin(origin);
+ dbCsca.setSubjectPrincipalName("admin_ch");
+ return dbCsca;
+ }
+
+ public static DbDsc getRsaDsc(int idSuffix, String origin, long fkCsca) {
+ final var dbDsc = new DbDsc();
+ dbDsc.setKeyId("keyid_" + idSuffix);
+ dbDsc.setFkCsca(fkCsca);
+ dbDsc.setCertificateRaw("cert");
+ dbDsc.setOrigin(origin);
+ dbDsc.setUse("sig");
+ dbDsc.setAlg(Algorithm.RS256);
+ dbDsc.setN("n");
+ dbDsc.setE("e");
+ dbDsc.setSubjectPublicKeyInfo("pk");
+ return dbDsc;
+ }
+
+ public static DbDsc getEcDsc(int idSuffix, String origin, long fkCsca) {
+ final var dbDsc = new DbDsc();
+ dbDsc.setKeyId("keyid_" + idSuffix);
+ dbDsc.setFkCsca(fkCsca);
+ dbDsc.setCertificateRaw("cert");
+ dbDsc.setOrigin(origin);
+ dbDsc.setUse("sig");
+ dbDsc.setAlg(Algorithm.ES256);
+ dbDsc.setCrv("crv");
+ dbDsc.setX("x");
+ dbDsc.setY("y");
+ return dbDsc;
+ }
+}
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/ActiveCertsResponse.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/ActiveCertsResponse.java
index e2369726..5a8604da 100644
--- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/ActiveCertsResponse.java
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/ActiveCertsResponse.java
@@ -24,6 +24,8 @@ public class ActiveCertsResponse {
example = "172800000")
private Duration validDuration = Duration.ofHours(48);
+ public ActiveCertsResponse() {}
+
public ActiveCertsResponse(List activeKeyIds) {
if (activeKeyIds == null) {
activeKeyIds = new ArrayList<>();
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/CertsResponse.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/CertsResponse.java
index 89147b88..2e9ba100 100644
--- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/CertsResponse.java
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/CertsResponse.java
@@ -16,6 +16,8 @@
public class CertsResponse {
private List certs = new ArrayList<>();
+ public CertsResponse() {}
+
public CertsResponse(List certs) {
if (certs == null) {
certs = new ArrayList<>();
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml
index 720b3fe7..725e3f71 100644
--- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml
@@ -144,6 +144,14 @@
bcpkix-jdk15on
1.68
+
+
+ ch.admin.bag.covidcertificate
+ ch-covidcertificate-backend-verifier-data
+ 1.0.0-SNAPSHOT
+ test-jar
+ test
+
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2JsonTest.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2JsonTest.java
new file mode 100644
index 00000000..9292cc30
--- /dev/null
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2JsonTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021 Ubique Innovation AG
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package ch.admin.bag.covidcertificate.backend.verifier.ws.controller;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+
+@ActiveProfiles({"actuator-security"})
+@SpringBootTest(
+ properties = {
+ "ws.monitor.prometheus.user=prometheus",
+ "ws.monitor.prometheus.password=prometheus",
+ "management.endpoints.enabled-by-default=true",
+ "management.endpoints.web.exposure.include=*"
+ })
+@TestInstance(Lifecycle.PER_CLASS)
+public class KeyControllerV2JsonTest extends KeyControllerV2Test {
+
+ @BeforeAll
+ public void setup() {
+ super.setup();
+ this.acceptMediaType = MediaType.APPLICATION_JSON;
+ }
+}
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2JwsTest.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2JwsTest.java
new file mode 100644
index 00000000..aa12a82c
--- /dev/null
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2JwsTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021 Ubique Innovation AG
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package ch.admin.bag.covidcertificate.backend.verifier.ws.controller;
+
+import ch.admin.bag.covidcertificate.backend.verifier.ws.security.signature.JwsMessageConverter;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@ActiveProfiles({"actuator-security"})
+@SpringBootTest(
+ properties = {
+ "ws.monitor.prometheus.user=prometheus",
+ "ws.monitor.prometheus.password=prometheus",
+ "management.endpoints.enabled-by-default=true",
+ "management.endpoints.web.exposure.include=*"
+ })
+@TestInstance(Lifecycle.PER_CLASS)
+public class KeyControllerV2JwsTest extends KeyControllerV2Test {
+
+ @BeforeAll
+ public void setup() {
+ super.setup();
+ this.acceptMediaType = JwsMessageConverter.JWS_MEDIA_TYPE;
+ }
+}
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2Test.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2Test.java
new file mode 100644
index 00000000..f5b0b2ca
--- /dev/null
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/KeyControllerV2Test.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (c) 2021 Ubique Innovation AG
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package ch.admin.bag.covidcertificate.backend.verifier.ws.controller;
+
+import static ch.admin.bag.covidcertificate.backend.verifier.data.util.TestUtil.getDefaultCsca;
+import static ch.admin.bag.covidcertificate.backend.verifier.data.util.TestUtil.getEcDsc;
+import static ch.admin.bag.covidcertificate.backend.verifier.data.util.TestUtil.getRsaDsc;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import ch.admin.bag.covidcertificate.backend.verifier.data.VerifierDataService;
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.ActiveCertsResponse;
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.CertFormat;
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.CertsResponse;
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.ClientCert;
+import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.DbDsc;
+import ch.admin.bag.covidcertificate.backend.verifier.ws.util.TestHelper;
+import ch.admin.bag.covidcertificate.backend.verifier.ws.utils.CacheUtil;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+import org.apache.http.HttpHeaders;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.web.servlet.ResultMatcher;
+import org.springframework.transaction.annotation.Transactional;
+
+public abstract class KeyControllerV2Test extends BaseControllerTest {
+
+ @Autowired protected VerifierDataService verifierDataService;
+
+ protected MediaType acceptMediaType;
+
+ private static final String BASE_URL = "/trust/v2/keys/";
+ private static final String UPDATES_ENDPOINT = "updates";
+ private static final String LIST_ENDPOINT = "list";
+
+ private static final String NEXT_SINCE_HEADER = "X-Next-Since";
+ private static final String UP_TO_DATE_HEADER = "up-to-date";
+ private static final String UP_TO_HEADER = "up-to";
+
+ private static final String UP_TO_QUERY_PARAM = "upTo";
+ private static final String CERT_FORMAT_QUERY_PARAM = "certFormat";
+ private static final String SINCE_QUERY_PARAM = "since";
+
+ private static final String ORIGIN_CH = "CH";
+
+ private Random rand = new Random();
+ private List suffixes = new ArrayList<>();
+
+ @BeforeAll
+ public void setup() {
+ for (int i = 0; i < verifierDataService.getMaxDscBatchCount() * 10; i++) {
+ suffixes.add(i);
+ }
+ }
+
+ @Test
+ void helloTest() throws Exception {
+ final MockHttpServletResponse response =
+ mockMvc.perform(get(BASE_URL).accept(MediaType.TEXT_PLAIN))
+ .andExpect(status().is2xxSuccessful())
+ .andReturn()
+ .getResponse();
+
+ assertNotNull(response);
+ assertEquals("Hello from CH Covidcertificate Verifier WS", response.getContentAsString());
+ }
+
+ @Test
+ public void keysUpdatesInvalidRequestTest() throws Exception {
+ ResultMatcher expected = status().is4xxClientError();
+ // missing param `certFormat`
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .accept(acceptMediaType)
+ .queryParam(UP_TO_QUERY_PARAM, String.valueOf(rand.nextInt())))
+ .andExpect(expected)
+ .andReturn();
+
+ // missing param `upTo`
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .accept(acceptMediaType)
+ .queryParam(CERT_FORMAT_QUERY_PARAM, CertFormat.ANDROID.name()))
+ .andExpect(expected)
+ .andReturn();
+
+ // invalid value `certFormat`
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .queryParam(UP_TO_QUERY_PARAM, String.valueOf(rand.nextInt()))
+ .queryParam(CERT_FORMAT_QUERY_PARAM, "windows")
+ .accept(acceptMediaType))
+ .andExpect(expected)
+ .andReturn();
+
+ // invalid value `upTo`
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .queryParam(UP_TO_QUERY_PARAM, "a")
+ .queryParam(CERT_FORMAT_QUERY_PARAM, CertFormat.ANDROID.name())
+ .accept(acceptMediaType))
+ .andExpect(expected)
+ .andReturn();
+
+ // invalid value `since`
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .queryParam(SINCE_QUERY_PARAM, "a")
+ .queryParam(UP_TO_QUERY_PARAM, String.valueOf(rand.nextInt()))
+ .queryParam(CERT_FORMAT_QUERY_PARAM, CertFormat.ANDROID.name())
+ .accept(acceptMediaType))
+ .andExpect(expected)
+ .andReturn();
+ }
+
+ @Test
+ public void keyUpdatesValidRequestTest() throws Exception {
+ ResultMatcher expected = status().is2xxSuccessful();
+ // valid
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .queryParam(UP_TO_QUERY_PARAM, String.valueOf(rand.nextInt()))
+ .queryParam(CERT_FORMAT_QUERY_PARAM, CertFormat.ANDROID.name())
+ .accept(acceptMediaType))
+ .andExpect(expected)
+ .andReturn();
+
+ // valid
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .queryParam(SINCE_QUERY_PARAM, String.valueOf(rand.nextInt()))
+ .queryParam(UP_TO_QUERY_PARAM, String.valueOf(rand.nextInt()))
+ .queryParam(CERT_FORMAT_QUERY_PARAM, CertFormat.ANDROID.name())
+ .accept(acceptMediaType))
+ .andExpect(expected)
+ .andReturn();
+ }
+
+ @Test
+ @Transactional
+ public void keysUpdatesTest() throws Exception {
+ // fill db
+ final Long cscaId = insertCsca();
+ List dscs = insertNDscs(cscaId, 15);
+ int minPkId = (int) verifierDataService.findMaxDscPkId() - dscs.size();
+
+ // get keys updates (all no since)
+ Integer since = null;
+ int upTo = (int) verifierDataService.findMaxDscPkId();
+ MockHttpServletResponse response = getKeysUpdates(since, upTo);
+ // verify response
+ assertUpdatesResponse(response, dscs, since, upTo, true);
+
+ // get keys updates (all with since)
+ since = minPkId;
+ response = getKeysUpdates(since, upTo);
+ // verify response
+ assertUpdatesResponse(response, dscs, since, upTo, true);
+
+ // get keys updates (cut off with since)
+ since = minPkId + rand.nextInt(7);
+ response = getKeysUpdates(since, upTo);
+ // verify response
+ assertUpdatesResponse(response, dscs, since, upTo, true);
+
+ // get keys updates (cut off with since and upTo)
+ upTo = (int) verifierDataService.findMaxDscPkId() - rand.nextInt(7);
+ response = getKeysUpdates(since, upTo);
+ // verify response
+ assertUpdatesResponse(response, dscs, since, upTo, true);
+
+ // get keys updates (cut off with upTo)
+ since = minPkId;
+ response = getKeysUpdates(since, upTo);
+ // verify response
+ assertUpdatesResponse(response, dscs, since, upTo, true);
+
+ // test batching
+ int batchCount = 4;
+ dscs.addAll(
+ insertNDscs(cscaId, verifierDataService.getMaxDscBatchCount() * (batchCount - 1)));
+
+ // upTo set so no batching kicks in
+ since = null;
+ response = getKeysUpdates(since, upTo);
+ // verify response
+ assertUpdatesResponse(response, dscs, since, upTo, true);
+
+ // upTo set so everything is returned and batching is required (page through)
+ upTo = (int) verifierDataService.findMaxDscPkId();
+ for (int i = 1; i <= batchCount; i++) {
+ response = getKeysUpdates(since, upTo);
+ // verify response
+ assertUpdatesResponse(response, dscs, since, upTo, i == batchCount);
+ since = Integer.valueOf(response.getHeader(NEXT_SINCE_HEADER));
+ }
+ }
+
+ private MockHttpServletResponse getKeysUpdates(Integer since, int upTo) throws Exception {
+ String sinceStr = since != null ? String.valueOf(since) : "";
+ return mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .queryParam(SINCE_QUERY_PARAM, sinceStr)
+ .queryParam(UP_TO_QUERY_PARAM, String.valueOf(upTo))
+ .queryParam(CERT_FORMAT_QUERY_PARAM, CertFormat.ANDROID.name())
+ .accept(acceptMediaType))
+ .andExpect(status().is2xxSuccessful())
+ .andReturn()
+ .getResponse();
+ }
+
+ private void assertUpdatesResponse(
+ MockHttpServletResponse response,
+ List dscs,
+ Integer since,
+ int upTo,
+ boolean expectedUpToDate)
+ throws Exception {
+ if (since == null) {
+ since = 0;
+ }
+ assertNotNull(response);
+ List certs =
+ testHelper
+ .verifyAndReadValue(
+ response,
+ acceptMediaType,
+ TestHelper.PATH_TO_CA_PEM,
+ CertsResponse.class)
+ .getCerts();
+
+ // assert count
+ int lowerCutCount =
+ Math.max(0, since - ((int) verifierDataService.findMaxDscPkId() - dscs.size()));
+ int upperCutCount = Math.max(0, (int) verifierDataService.findMaxDscPkId() - upTo);
+ int expectedSize =
+ Math.min(
+ verifierDataService.getMaxDscBatchCount(),
+ Math.max(0, dscs.size() - lowerCutCount - upperCutCount));
+ assertEquals(expectedSize, certs.size());
+
+ // assert certs
+ if (expectedSize > 0) {
+ List expectedKeyIds = new ArrayList<>();
+ for (int i = lowerCutCount; i < (expectedSize + lowerCutCount); i++) {
+ expectedKeyIds.add(dscs.get(i).getKeyId());
+ }
+ List actualKeyIds =
+ certs.stream().map(ClientCert::getKeyId).collect(Collectors.toList());
+ assertTrue(expectedKeyIds.containsAll(actualKeyIds));
+ }
+
+ // assert headers
+ assertEquals(expectedUpToDate ? "true" : null, response.getHeader(UP_TO_DATE_HEADER));
+ assertEquals(
+ String.valueOf(
+ Math.max((int) verifierDataService.findMaxDscPkId() - dscs.size(), since)
+ + expectedSize),
+ response.getHeader(NEXT_SINCE_HEADER));
+ assertMaxAge(response, CacheUtil.KEYS_UPDATES_MAX_AGE);
+ }
+
+ private void assertMaxAge(MockHttpServletResponse response, Duration expectedDuration) {
+ assertEquals(
+ "max-age=" + expectedDuration.get(ChronoUnit.SECONDS),
+ response.getHeader("Cache-Control"));
+ }
+
+ @Test
+ @Transactional
+ public void keysListAndUpdatesUpToTest() throws Exception {
+ // fill db
+ final Long cscaId = insertCsca();
+ List dscs = insertSomeDscs(cscaId);
+
+ // get active keys
+ MockHttpServletResponse response =
+ mockMvc.perform(get(BASE_URL + LIST_ENDPOINT).accept(acceptMediaType))
+ .andExpect(status().is2xxSuccessful())
+ .andReturn()
+ .getResponse();
+
+ // verify response
+ assertNotNull(response);
+ ActiveCertsResponse activeCerts =
+ testHelper.verifyAndReadValue(
+ response,
+ acceptMediaType,
+ TestHelper.PATH_TO_CA_PEM,
+ ActiveCertsResponse.class);
+
+ List expectedActiveKeyIds =
+ dscs.stream().map(DbDsc::getKeyId).collect(Collectors.toList());
+ List activeKeyIds = activeCerts.getActiveKeyIds();
+ assertEquals(expectedActiveKeyIds.size(), activeKeyIds.size());
+ assertTrue(expectedActiveKeyIds.containsAll(activeKeyIds));
+
+ assertEquals(Duration.ofHours(48).toMillis(), activeCerts.getValidDuration());
+
+ String upTo = response.getHeader(UP_TO_HEADER);
+ assertEquals(String.valueOf((int) verifierDataService.findMaxDscPkId()), upTo);
+ assertMaxAge(response, CacheUtil.KEYS_LIST_MAX_AGE);
+
+ // insert new dscs
+ List newDscs = insertSomeDscs(cscaId);
+
+ // get keys updates up to
+ response =
+ mockMvc.perform(
+ get(BASE_URL + UPDATES_ENDPOINT)
+ .queryParam(UP_TO_QUERY_PARAM, upTo)
+ .queryParam(
+ CERT_FORMAT_QUERY_PARAM, CertFormat.ANDROID.name())
+ .accept(acceptMediaType))
+ .andExpect(status().is2xxSuccessful())
+ .andReturn()
+ .getResponse();
+
+ // verify response
+ CertsResponse certs =
+ testHelper.verifyAndReadValue(
+ response, acceptMediaType, TestHelper.PATH_TO_CA_PEM, CertsResponse.class);
+ List clientCerts = certs.getCerts();
+ assertEquals(expectedActiveKeyIds.size(), clientCerts.size());
+ assertTrue(
+ expectedActiveKeyIds.containsAll(
+ clientCerts.stream()
+ .map(ClientCert::getKeyId)
+ .collect(Collectors.toList())));
+
+ // get active keys again
+ response =
+ mockMvc.perform(get(BASE_URL + LIST_ENDPOINT).accept(acceptMediaType))
+ .andExpect(status().is2xxSuccessful())
+ .andReturn()
+ .getResponse();
+
+ // verify response
+ activeCerts =
+ testHelper.verifyAndReadValue(
+ response,
+ acceptMediaType,
+ TestHelper.PATH_TO_CA_PEM,
+ ActiveCertsResponse.class);
+
+ expectedActiveKeyIds.addAll(
+ newDscs.stream().map(DbDsc::getKeyId).collect(Collectors.toList()));
+ activeKeyIds = activeCerts.getActiveKeyIds();
+ assertEquals(expectedActiveKeyIds.size(), activeKeyIds.size());
+ assertTrue(expectedActiveKeyIds.containsAll(activeKeyIds));
+
+ upTo = response.getHeader(UP_TO_HEADER);
+ assertEquals(String.valueOf((int) verifierDataService.findMaxDscPkId()), upTo);
+ }
+
+ @Test
+ @Transactional
+ public void notModifiedTest() throws Exception {
+ // get current etag
+ MockHttpServletResponse response =
+ mockMvc.perform(
+ get(BASE_URL + LIST_ENDPOINT)
+ .accept(acceptMediaType)
+ .header(HttpHeaders.IF_NONE_MATCH, "random"))
+ .andExpect(status().is2xxSuccessful())
+ .andReturn()
+ .getResponse();
+
+ // test not modified
+ String etag = response.getHeader(HttpHeaders.ETAG);
+ mockMvc.perform(
+ get(BASE_URL + LIST_ENDPOINT)
+ .accept(acceptMediaType)
+ .header(HttpHeaders.IF_NONE_MATCH, etag))
+ .andExpect(status().isNotModified())
+ .andReturn()
+ .getResponse();
+
+ // add more dscs
+ final Long cscaId = insertCsca();
+ insertSomeDscs(cscaId);
+
+ // get current etag
+ response =
+ mockMvc.perform(
+ get(BASE_URL + LIST_ENDPOINT)
+ .accept(acceptMediaType)
+ .header(HttpHeaders.IF_NONE_MATCH, etag))
+ .andExpect(status().is2xxSuccessful())
+ .andReturn()
+ .getResponse();
+
+ // test not modified
+ etag = response.getHeader(HttpHeaders.ETAG);
+ mockMvc.perform(
+ get(BASE_URL + LIST_ENDPOINT)
+ .accept(acceptMediaType)
+ .header(HttpHeaders.IF_NONE_MATCH, etag))
+ .andExpect(status().isNotModified())
+ .andReturn()
+ .getResponse();
+ }
+
+ private Long insertCsca() {
+ verifierDataService.insertCscas(Collections.singletonList(getDefaultCsca(0, ORIGIN_CH)));
+ return verifierDataService.findCscas(ORIGIN_CH).get(0).getId();
+ }
+
+ private List insertNDscs(Long cscaId, Integer numToInsert) {
+ List dscs = new ArrayList<>();
+ dscs.addAll(
+ getRandomSuffixes().stream()
+ .map(s -> getRsaDsc(s, ORIGIN_CH, cscaId))
+ .collect(Collectors.toList()));
+ dscs.addAll(
+ getRandomSuffixes().stream()
+ .map(s -> getEcDsc(s, ORIGIN_CH, cscaId))
+ .collect(Collectors.toList()));
+ if (numToInsert != null) {
+ while (dscs.size() < numToInsert) {
+ dscs.addAll(
+ getRandomSuffixes().stream()
+ .map(s -> getEcDsc(s, ORIGIN_CH, cscaId))
+ .collect(Collectors.toList()));
+ }
+ dscs = dscs.subList(0, numToInsert);
+ }
+ verifierDataService.insertDscs(dscs);
+ return dscs;
+ }
+
+ private List insertSomeDscs(Long cscaId) {
+ return insertNDscs(cscaId, null);
+ }
+
+ /**
+ * returns 1-10 random suffixes between 1 and 1000. no duplicates are returned within the scope
+ * of this test class
+ */
+ private List getRandomSuffixes() {
+ if (suffixes.isEmpty()) {
+ throw new RuntimeException("need more suffixes");
+ }
+
+ List randomSuffixes = new ArrayList<>();
+
+ final int minCount = 1;
+ final int maxCount = 10;
+ for (int i = 0; i < rand.nextInt(maxCount - minCount) + minCount; i++) {
+ int randIndex = rand.nextInt(suffixes.size());
+ randomSuffixes.add(suffixes.get(randIndex));
+ suffixes.remove(randIndex);
+ }
+ return randomSuffixes;
+ }
+
+ @Override
+ protected String getUrlForSecurityHeadersTest() {
+ return BASE_URL;
+ }
+
+ @Override
+ protected MediaType getSecurityHeadersRequestMediaType() {
+ return MediaType.TEXT_PLAIN;
+ }
+}
diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/application-test.properties b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/application-test.properties
index 0f005c1b..3366b5c4 100644
--- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/application-test.properties
+++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/application-test.properties
@@ -1 +1,4 @@
-revocationList.baseurl=https://testurl.admin.ch/api/
\ No newline at end of file
+revocationList.baseurl=https://testurl.admin.ch/api/
+
+ws.keys.update.max-age=PT2M
+ws.keys.list.max-age=PT3M
\ No newline at end of file