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