diff --git a/common/persistence/pom.xml b/common/persistence/pom.xml index 40d75cfb08..45f78c166d 100644 --- a/common/persistence/pom.xml +++ b/common/persistence/pom.xml @@ -1,5 +1,6 @@ - common @@ -123,6 +124,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M3 + org.apache.maven.plugins maven-site-plugin diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java index 3cd4213230..c4cd553747 100644 --- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java +++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java @@ -50,7 +50,8 @@ public class DiagnosisKey { * reject any diagnosis keys that do not have a rolling period of a certain fixed value. See * https://developer.apple.com/documentation/exposurenotification/setting_up_an_exposure_notification_server */ - public static final int EXPECTED_ROLLING_PERIOD = 144; + public static final int MIN_ROLLING_PERIOD = 0; + public static final int MAX_ROLLING_PERIOD = 144; private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); @@ -61,8 +62,8 @@ public class DiagnosisKey { @ValidRollingStartIntervalNumber private final int rollingStartIntervalNumber; - @Range(min = EXPECTED_ROLLING_PERIOD, max = EXPECTED_ROLLING_PERIOD, - message = "Rolling period must be " + EXPECTED_ROLLING_PERIOD + ".") + @Range(min = MIN_ROLLING_PERIOD, max = MAX_ROLLING_PERIOD, + message = "Rolling period must be between " + MIN_ROLLING_PERIOD + " and " + MAX_ROLLING_PERIOD + ".") private final int rollingPeriod; @Range(min = 0, max = 8, message = "Risk level must be between 0 and 8.") diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java index a2fb441858..efcf51d36f 100644 --- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java +++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java @@ -48,7 +48,7 @@ public class DiagnosisKeyBuilder implements private byte[] keyData; private int rollingStartIntervalNumber; - private int rollingPeriod = DiagnosisKey.EXPECTED_ROLLING_PERIOD; + private int rollingPeriod = DiagnosisKey.MAX_ROLLING_PERIOD; private int transmissionRiskLevel; private Long submissionTimestamp = null; private String countryCode; diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilders.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilders.java index 36ecb90b3d..176c4f26de 100644 --- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilders.java +++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilders.java @@ -92,7 +92,7 @@ interface FinalBuilder { /** * Adds the specified rolling period to this builder. If not specified, the rolling period defaults to {@link - * DiagnosisKey#EXPECTED_ROLLING_PERIOD} + * DiagnosisKey#MAX_ROLLING_PERIOD} * * @param rollingPeriod Number describing how long a key is valid. It is expressed in increments of 10 minutes (e.g. * 144 for 24 hours). diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java index cd5abae820..69d326909a 100644 --- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java +++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/repository/DiagnosisKeyRepository.java @@ -30,15 +30,6 @@ @Repository public interface DiagnosisKeyRepository extends PagingAndSortingRepository { - /** - * Counts all entries that have a submission timestamp less or equal than the specified one. - * - * @param submissionTimestamp The submission timestamp up to which entries will be expired. - * @return The number of expired keys. - */ - @Query("SELECT COUNT(*) FROM diagnosis_key WHERE submission_timestamp<=:threshold") - int countOlderThanOrEqual(@Param("threshold") long submissionTimestamp); - /** * Counts all entries that have a submission timestamp less or equal than the specified one * and match the given country_code. @@ -46,9 +37,9 @@ public interface DiagnosisKeyRepository extends PagingAndSortingRepository findAllKeysWhereVisitedCountryContains(@Param("country_code") String countryCode); - /** - * Deletes all entries that have a submission timestamp less or equal than the specified one. - * - * @param submissionTimestamp The submission timestamp up to which entries will be deleted. - */ - @Modifying - @Query("DELETE FROM diagnosis_key WHERE submission_timestamp<=:threshold") - void deleteOlderThanOrEqual(@Param("threshold") long submissionTimestamp); - /** * Deletes all entries that have a submission timestamp less or equal than the specified one * and match the origin country_code. @@ -76,8 +58,8 @@ public interface DiagnosisKeyRepository extends PagingAndSortingRepository keyWithRollingPeriod(invalidRollingPeriod))) .isInstanceOf(InvalidDiagnosisKeyException.class) - .hasMessage("[Rolling period must be " + DiagnosisKey.EXPECTED_ROLLING_PERIOD + .hasMessage("[Rolling period must be between "+ DiagnosisKey.MIN_ROLLING_PERIOD + " and " + DiagnosisKey.MAX_ROLLING_PERIOD + ". Invalid Value: " + invalidRollingPeriod + "]"); } - @Test - void rollingPeriodDoesNotThrowForValid() { - assertThatCode(() -> keyWithRollingPeriod(DiagnosisKey.EXPECTED_ROLLING_PERIOD)).doesNotThrowAnyException(); + @ParameterizedTest + @ValueSource(ints = {DiagnosisKey.MIN_ROLLING_PERIOD, 100, DiagnosisKey.MAX_ROLLING_PERIOD}) + void rollingPeriodDoesNotThrowForValid(int validRollingPeriod) { + assertThatCode(() -> keyWithRollingPeriod(validRollingPeriod)).doesNotThrowAnyException(); } @ParameterizedTest @@ -218,7 +219,7 @@ void submissionTimestampDoesNotThrowOnValid() { () -> buildDiagnosisKeyForSubmissionTimestamp(Instant.now().minus(Duration.ofHours(2)).getEpochSecond() / SECONDS_PER_HOUR)) .doesNotThrowAnyException(); } - + private DiagnosisKey keyWithKeyData(byte[] expKeyData) { return DiagnosisKey.builder() .withKeyData(expKeyData) @@ -260,7 +261,7 @@ private void assertDiagnosisKeyEquals(DiagnosisKey actDiagnosisKey, long expSubm assertThat(actDiagnosisKey.getSubmissionTimestamp()).isEqualTo(expSubmissionTimestamp); assertThat(actDiagnosisKey.getKeyData()).isEqualTo(this.expKeyData); assertThat(actDiagnosisKey.getRollingStartIntervalNumber()).isEqualTo(this.expRollingStartIntervalNumber); - assertThat(actDiagnosisKey.getRollingPeriod()).isEqualTo(DiagnosisKey.EXPECTED_ROLLING_PERIOD); + assertThat(actDiagnosisKey.getRollingPeriod()).isEqualTo(DiagnosisKey.MAX_ROLLING_PERIOD); assertThat(actDiagnosisKey.getTransmissionRiskLevel()).isEqualTo(this.expTransmissionRiskLevel); } } diff --git a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyServiceMockedRepositoryTest.java b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyServiceMockedRepositoryTest.java index d3b8243ea7..20ad98c83f 100644 --- a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyServiceMockedRepositoryTest.java +++ b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyServiceMockedRepositoryTest.java @@ -91,14 +91,14 @@ private void mockInvalidKeyInDb(List keys) { private DiagnosisKey validKey(long expSubmissionTimestamp) { return new DiagnosisKey(expKeyData, expRollingStartIntervalNumber, - DiagnosisKey.EXPECTED_ROLLING_PERIOD, expTransmissionRiskLevel, expSubmissionTimestamp, false, + DiagnosisKey.MAX_ROLLING_PERIOD, expTransmissionRiskLevel, expSubmissionTimestamp, false, originCountry, visitedCountries, reportType, daysSinceOnsetOfSymptoms); } private DiagnosisKey invalidKey(long expSubmissionTimestamp) { byte[] expKeyData = "17--bytelongarray".getBytes(StandardCharsets.US_ASCII); return new DiagnosisKey(expKeyData, expRollingStartIntervalNumber, - DiagnosisKey.EXPECTED_ROLLING_PERIOD, expTransmissionRiskLevel, expSubmissionTimestamp, false, + DiagnosisKey.MAX_ROLLING_PERIOD, expTransmissionRiskLevel, expSubmissionTimestamp, false, originCountry, visitedCountries, reportType, daysSinceOnsetOfSymptoms); } } diff --git a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java index 8aade877d9..f981d24703 100644 --- a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java +++ b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java @@ -119,7 +119,7 @@ void testApplyRetentionPolicyForEmptyDb() { @Test void testApplyRetentionPolicyForOneNotApplicableEntry() { - var expKeys = List.of(buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusHours(23))); + var expKeys = List.of(buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L))); diagnosisKeyService.saveDiagnosisKeys(expKeys); diagnosisKeyService.applyRetentionPolicy(1, "DE"); @@ -130,7 +130,7 @@ void testApplyRetentionPolicyForOneNotApplicableEntry() { @Test void testApplyRetentionPolicyForOneApplicableEntry() { - var keys = List.of(buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L))); + var keys = List.of(buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L).minusHours(1))); diagnosisKeyService.saveDiagnosisKeys(keys); diagnosisKeyService.applyRetentionPolicy(1, "DE"); @@ -154,7 +154,7 @@ void testShouldNotDeleteKeysFromAnotherCountry() { @Test void testShouldDeleteKeysWithMatchingVisitedCountry() { - var frenchKeys = buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L), "DE", Collections.singletonList("FR"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS); + var frenchKeys = buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L).minusHours(1), "DE", Collections.singletonList("FR"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS); var germanKeys = buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(2L), "DE", Collections.singletonList("DE"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS); diagnosisKeyService.saveDiagnosisKeys(List.of(germanKeys, frenchKeys)); @@ -167,7 +167,7 @@ void testShouldDeleteKeysWithMatchingVisitedCountry() { @Test void testShouldDeleteKeysWhereAnyOfVisitedCountriesMatch() { var keys = List.of( - buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L), "DE", List.of("DE", "FR", "LU"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS)); + buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L).minusHours(1), "DE", List.of("DE", "FR", "LU"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS)); diagnosisKeyService.saveDiagnosisKeys(keys); diagnosisKeyService.applyRetentionPolicy(1, "FR"); var actKeys = diagnosisKeyService.getDiagnosisKeys(); @@ -177,9 +177,9 @@ void testShouldDeleteKeysWhereAnyOfVisitedCountriesMatch() { @Test void testShouldDeleteKeysFromDifferentOriginCountriesWithMatchingVisitedCountry() { var keys = List.of( - buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L), "DE", List.of("FR"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS), - buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L), "FR", List.of("FR"),ReportType.CONFIRMED_CLINICAL_DIAGNOSIS), - buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L), "LU", List.of("FR"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS)); + buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L).minusHours(1L), "DE", List.of("FR"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS), + buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L).minusHours(1L), "FR", List.of("FR"),ReportType.CONFIRMED_CLINICAL_DIAGNOSIS), + buildDiagnosisKeyForDateTime(OffsetDateTime.now(UTC).minusDays(1L).minusHours(1L), "LU", List.of("FR"), ReportType.CONFIRMED_CLINICAL_DIAGNOSIS)); diagnosisKeyService.saveDiagnosisKeys(keys); diagnosisKeyService.applyRetentionPolicy(1, "FR"); var actKeys = diagnosisKeyService.getDiagnosisKeys(); diff --git a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java index dff4fee327..d12af58012 100644 --- a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java +++ b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java @@ -31,6 +31,9 @@ public class DiagnosisKeyServiceTestHelper { + private static final Random random = new Random(); + + public static void assertDiagnosisKeysEqual(List expKeys, List actKeys) { assertThat(actKeys).withFailMessage("Cardinality mismatch").hasSameSizeAs(expKeys); @@ -56,7 +59,6 @@ public static void assertDiagnosisKeysEqual(List expKeys, public static DiagnosisKey buildDiagnosisKeyForSubmissionTimestamp(long submissionTimeStamp, boolean consentToFederation, String countryCode, List visitedCountries, ReportType reportType) { byte[] randomBytes = new byte[16]; - Random random = new Random(submissionTimeStamp); random.nextBytes(randomBytes); return DiagnosisKey.builder() .withKeyData(randomBytes) diff --git a/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/app_config.proto b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/app_config.proto index 2aa33b9a00..f9620498c1 100644 --- a/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/app_config.proto +++ b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/app_config.proto @@ -6,6 +6,7 @@ import "app/coronawarn/server/common/protocols/internal/risk_score_classificatio import "app/coronawarn/server/common/protocols/internal/risk_score_parameters.proto"; import "app/coronawarn/server/common/protocols/internal/app_version_config.proto"; import "app/coronawarn/server/common/protocols/internal/attenuation_duration.proto"; +import "app/coronawarn/server/common/protocols/internal/app_features.proto"; message ApplicationConfiguration { @@ -18,4 +19,8 @@ message ApplicationConfiguration { app.coronawarn.server.common.protocols.internal.AttenuationDuration attenuationDuration = 4; app.coronawarn.server.common.protocols.internal.ApplicationVersionConfiguration appVersion = 5; + + app.coronawarn.server.common.protocols.internal.AppFeatures appFeatures = 6; + + repeated string supportedCountries = 7; } diff --git a/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/app_features.proto b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/app_features.proto new file mode 100644 index 0000000000..9e1205609e --- /dev/null +++ b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/app_features.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +package app.coronawarn.server.common.protocols.internal; +option java_package = "app.coronawarn.server.common.protocols.internal"; +option java_multiple_files = true; + +message AppFeatures { + repeated AppFeature app_features = 1; +} + +message AppFeature { + string label = 1; + int32 value = 2; +} diff --git a/docs/SUBMISSION.md b/docs/SUBMISSION.md index 92be86cf51..84595e623b 100644 --- a/docs/SUBMISSION.md +++ b/docs/SUBMISSION.md @@ -37,6 +37,13 @@ You will find the implementation file at [`/services/submission/src/main/java/ap ### Validation Constraints -* `StartIntervalNumber` values from the same [`SubmissionPayload`](https://corona-warn-app.github.io/cwa-server/1.0.0/app/coronawarn/server/common/protocols/internal/SubmissionPayload.html) shall be unique. -* There must not be any keys in the [`SubmissionPayload`](https://corona-warn-app.github.io/cwa-server/1.0.0/app/coronawarn/server/common/protocols/internal/SubmissionPayload.html) that have overlapping time windows. -* The period covered by the data file must not exceed the configured maximum number of days, which is defined by `max-number-of-keys` property in [`application.yaml`](/services/submission/src/main/resources/application.yaml). Currently no submissions with more than 14 keys are accepted +Temporary Exposure Keys (TEK's) are submitted by the client device (iOS/Android phone) via the Submission Service. + +Constraints maintained as enviroment variables which are present as secrets in the Vault /cwa-server/submission + +The constraints put on submitted TEK's are as follows: + +* Each TEK contains a `StartIntervalNumber` (a date e.g. 2nd July 2020) +* The period covered by the data file must not exceed the configured maximum number of days, represented by the `MAX_NUMBER_OF_KEYS` property which is in the vault. +* The total combined rolling period for a single TEK cannot exceed maximum rolling period, represented by the `MAX_ROLLING_PERIOD` property which is in the vault. +* More than one TEK with the same `StartIntervalNumber` may be submitted, these will have their rolling period's combined. diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationPublicationConfig.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationPublicationConfig.java index ba5bddc29e..dab38047fc 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationPublicationConfig.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationPublicationConfig.java @@ -20,10 +20,20 @@ package app.coronawarn.server.services.distribution.assembly.appconfig; +import app.coronawarn.server.common.protocols.internal.AppFeatures; import app.coronawarn.server.common.protocols.internal.ApplicationConfiguration; +import app.coronawarn.server.common.protocols.internal.ApplicationConfiguration.Builder; +import app.coronawarn.server.common.protocols.internal.ApplicationVersionConfiguration; +import app.coronawarn.server.common.protocols.internal.ApplicationVersionInfo; +import app.coronawarn.server.common.protocols.internal.SemanticVersion; +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.AppVersions; +import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; + + /** * Provides the application configuration needed for the mobile client. Contains all necessary sub-configs, including: *
    @@ -49,7 +59,49 @@ public class ApplicationConfigurationPublicationConfig { * @throws UnableToLoadFileException when the file/transformation did not succeed */ @Bean - public ApplicationConfiguration createMasterConfiguration() throws UnableToLoadFileException { - return YamlLoader.loadYamlIntoProtobufBuilder(MASTER_FILE, ApplicationConfiguration.Builder.class).build(); + public ApplicationConfiguration createMasterConfiguration(DistributionServiceConfig distributionServiceConfig) + throws UnableToLoadFileException { + + return YamlLoader.loadYamlIntoProtobufBuilder(MASTER_FILE, Builder.class) + .setAppFeatures( + AppFeatures.newBuilder().addAllAppFeatures(distributionServiceConfig.getAppFeaturesProto()).build() + ) + .addAllSupportedCountries(List.of(distributionServiceConfig.getSupportedCountries())) + .setAppVersion(buildApplicationVersionConfiguration(distributionServiceConfig)) + .build(); + } + + /** + * Fetches the master configuration as a ApplicationConfiguration instance. + * + * @return test. + */ + public ApplicationVersionConfiguration buildApplicationVersionConfiguration( + DistributionServiceConfig distributionServiceConfig) { + AppVersions appVersions = distributionServiceConfig.getAppVersions(); + return ApplicationVersionConfiguration.newBuilder() + .setAndroid(buildApplicationVersionInfo(appVersions.getLatestAndroid(), appVersions.getMinAndroid())) + .setIos(buildApplicationVersionInfo(appVersions.getLatestIos(), appVersions.getMinIos())) + .build(); + } + + private ApplicationVersionInfo buildApplicationVersionInfo(String latestVersion, String minVersion) { + return ApplicationVersionInfo.newBuilder() + .setLatest(buildSemanticVersion(latestVersion)) + .setMin(buildSemanticVersion(minVersion)) + .build(); + } + + private SemanticVersion buildSemanticVersion(String version) { + return SemanticVersion.newBuilder() + .setMajor(getSemanticVersionNumber(version, 0)) + .setMinor(getSemanticVersionNumber(version, 1)) + .setPatch(getSemanticVersionNumber(version, 2)) + .build(); + } + + private int getSemanticVersionNumber(String version, int position) { + String[] items = version.split("\\."); + return Integer.valueOf(items[position]); } } diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java index 0eb5b97765..bc8802059b 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/config/DistributionServiceConfig.java @@ -21,6 +21,10 @@ package app.coronawarn.server.services.distribution.config; import app.coronawarn.server.common.protocols.external.exposurenotification.SignatureInfo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.Pattern; @@ -45,6 +49,7 @@ public class DistributionServiceConfig { private static final String ALGORITHM_OID_REGEX = "^[0-9]+[\\.[0-9]+]*$"; private static final String BUNDLE_REGEX = "^[a-z-]+[\\.[a-z-]+]*$"; private static final String PRIVATE_KEY_REGEX = "^(classpath:|file:[/]+)[a-zA-Z0-9_-]+[/[a-zA-Z0-9_-]+]*(.pem)?$"; + private static final String SUPPORTED_COUNTRY_CODES_REGEX = "^([a-zA-Z]{2}(\\,*[a-zA-Z]{2})*)$"; private Paths paths; private TestData testData; @@ -68,6 +73,10 @@ public class DistributionServiceConfig { private Signature signature; private Api api; private ObjectStore objectStore; + private List appFeatures; + @Pattern(regexp = SUPPORTED_COUNTRY_CODES_REGEX) + private String supportedCountries; + private AppVersions appVersions; public Paths getPaths() { return paths; @@ -174,6 +183,44 @@ public void setObjectStore( this.objectStore = objectStore; } + public List getAppFeatures() { + return appFeatures; + } + + public void setAppFeatures(List appFeatures) { + this.appFeatures = appFeatures; + } + + public String[] getSupportedCountries() { + return supportedCountries.split(","); + } + + public void setSupportedCountries(String supportedCountries) { + this.supportedCountries = supportedCountries; + } + + public AppVersions getAppVersions() { + return appVersions; + } + + public void setAppVersions(AppVersions appVersions) { + this.appVersions = appVersions; + } + + + /** + * Get app features as list of protobuf objects. + * + * @return list of {@link app.coronawarn.server.common.protocols.internal.AppFeature} + */ + public List getAppFeaturesProto() { + return getAppFeatures().stream() + .map(appFeature -> app.coronawarn.server.common.protocols.internal.AppFeature.newBuilder() + .setLabel(appFeature.getLabel()) + .setValue(appFeature.getValue()).build()) + .collect(Collectors.toList()); + } + public static class TekExport { @Pattern(regexp = FILE_NAME_WITH_TYPE_REGEX) @@ -548,4 +595,68 @@ public void setForceUpdateKeyfiles(Boolean forceUpdateKeyfiles) { this.forceUpdateKeyfiles = forceUpdateKeyfiles; } } + + private static class AppFeature { + + private String label; + private Integer value; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + } + + public static class AppVersions { + + private String latestIos; + private String minIos; + private String latestAndroid; + private String minAndroid; + + + public String getLatestIos() { + return latestIos; + } + + public void setLatestIos(String latestIos) { + this.latestIos = latestIos; + } + + public String getMinIos() { + return minIos; + } + + public void setMinIos(String minIos) { + this.minIos = minIos; + } + + public String getLatestAndroid() { + return latestAndroid; + } + + public void setLatestAndroid(String latestAndroid) { + this.latestAndroid = latestAndroid; + } + + public String getMinAndroid() { + return minAndroid; + } + + public void setMinAndroid(String minAndroid) { + this.minAndroid = minAndroid; + } + + } } diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/TestDataGeneration.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/TestDataGeneration.java index d06e5a257a..478ba43bc4 100644 --- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/TestDataGeneration.java +++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/runner/TestDataGeneration.java @@ -106,7 +106,7 @@ private void writeTestData() { // Timestamps in hours since epoch. Test data generation starts one hour after the latest diagnosis key in the // database and ends one hour before the current one. - long startTimestamp = getGeneratorStartTimestamp(existingDiagnosisKeys) + 1; // Inclusive + long startTimestamp = getGeneratorStartTimestamp(existingDiagnosisKeys); // Inclusive long endTimestamp = getGeneratorEndTimestamp(); // Inclusive // Add the startTimestamp to the seed. Otherwise we would generate the same data every hour. @@ -142,7 +142,7 @@ private long getGeneratorStartTimestamp(List diagnosisKeys) { return getRetentionStartTimestamp(); } else { DiagnosisKey latestDiagnosisKey = diagnosisKeys.get(diagnosisKeys.size() - 1); - return latestDiagnosisKey.getSubmissionTimestamp(); + return latestDiagnosisKey.getSubmissionTimestamp() + 1; } } diff --git a/services/distribution/src/main/resources/application.yaml b/services/distribution/src/main/resources/application.yaml index 0164201e28..3fcd4bff96 100644 --- a/services/distribution/src/main/resources/application.yaml +++ b/services/distribution/src/main/resources/application.yaml @@ -83,6 +83,16 @@ services: # Allows distribution to overwrite files which are published on the object store force-update-keyfiles: ${FORCE_UPDATE_KEYFILES:false} + app-features: + - label: reserved + value: ${reserved:1} + supported-countries: ${SUPPORTED_COUNTRIES:DE} + app-versions: + latest-ios: ${LATEST_IOS_VERSION:0.8.2} + min-ios: ${MIN_IOS_VERSION:0.5.0} + latest-android: ${LATEST_IOS_VERSION:1.0.4} + min-android: ${MIN_ANDROID_VERSION:1.0.4} + spring: main: web-application-type: NONE @@ -95,6 +105,7 @@ spring: datasource: driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://${POSTGRESQL_SERVICE_HOST}:${POSTGRESQL_SERVICE_PORT}/${POSTGRESQL_DATABASE}?ssl=true&sslmode=verify-full&sslrootcert=${SSL_POSTGRES_CERTIFICATE_PATH}&sslcert=${SSL_DISTRIBUTION_CERTIFICATE_PATH}&sslkey=${SSL_DISTRIBUTION_PRIVATE_KEY_PATH} username: ${POSTGRESQL_USER_DISTRIBUTION:local_setup_distribution} password: ${POSTGRESQL_PASSWORD_DISTRIBUTION:local_setup_distribution} diff --git a/services/distribution/src/main/resources/master-config/app-config.yaml b/services/distribution/src/main/resources/master-config/app-config.yaml index 79ae35f865..5e401598a6 100644 --- a/services/distribution/src/main/resources/master-config/app-config.yaml +++ b/services/distribution/src/main/resources/master-config/app-config.yaml @@ -14,4 +14,3 @@ min-risk-score: 11 attenuation-duration: !include attenuation-duration.yaml risk-score-classes: !include risk-score-classification.yaml exposure-config: !include exposure-config.yaml -app-version: !include app-version-config.yaml diff --git a/services/distribution/src/main/resources/master-config/app-version-config.yaml b/services/distribution/src/main/resources/master-config/app-version-config.yaml deleted file mode 100644 index 515a7841f8..0000000000 --- a/services/distribution/src/main/resources/master-config/app-version-config.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# This is the Application Version Configuration master file which contains information for clients -# about the latest and minimum supported mobile app versions on Android and iOS. -# -# The latest version must not be lower than the min version. -# -# Change this file with caution! - -ios: - latest: - major: 0 - minor: 8 - patch: 2 - min: - major: 0 - minor: 5 - patch: 0 -android: - latest: - major: 1 - minor: 0 - patch: 4 - min: - major: 1 - minor: 0 - patch: 4 diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationMasterFileTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationMasterFileTest.java index 07a297dd80..47c6eedde8 100644 --- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationMasterFileTest.java +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/ApplicationConfigurationMasterFileTest.java @@ -25,14 +25,19 @@ import app.coronawarn.server.common.protocols.internal.ApplicationConfiguration; import app.coronawarn.server.services.distribution.assembly.appconfig.validation.ApplicationConfigurationValidator; import app.coronawarn.server.services.distribution.assembly.appconfig.validation.ValidationResult; +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +@EnableConfigurationProperties(value = DistributionServiceConfig.class) @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = ApplicationConfigurationPublicationConfig.class) +@ContextConfiguration(classes = ApplicationConfigurationPublicationConfig.class, + initializers = ConfigFileApplicationContextInitializer.class) class ApplicationConfigurationMasterFileTest { private static final ValidationResult SUCCESS = new ValidationResult(); diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ApplicationVersionConfigurationValidatorTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ApplicationVersionConfigurationValidatorTest.java index c75e0a9144..655963ba3f 100644 --- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ApplicationVersionConfigurationValidatorTest.java +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ApplicationVersionConfigurationValidatorTest.java @@ -26,36 +26,128 @@ import static org.assertj.core.api.Assertions.assertThat; import app.coronawarn.server.common.protocols.internal.ApplicationVersionConfiguration; -import app.coronawarn.server.services.distribution.assembly.appconfig.UnableToLoadFileException; -import app.coronawarn.server.services.distribution.assembly.appconfig.YamlLoader; +import app.coronawarn.server.services.distribution.assembly.appconfig.ApplicationConfigurationPublicationConfig; import app.coronawarn.server.services.distribution.assembly.appconfig.validation.ValidationError.ErrorType; +import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.stream.Stream; + +@EnableConfigurationProperties(value = DistributionServiceConfig.class) +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {DistributionServiceConfig.class,ApplicationConfigurationPublicationConfig.class}, + initializers = ConfigFileApplicationContextInitializer.class) class ApplicationVersionConfigurationValidatorTest { private static final ValidationResult SUCCESS = new ValidationResult(); - @Test - void succeedsIfLatestEqualsMin() throws UnableToLoadFileException { - var validator = buildValidator("app-version/latest-equals-min.yaml"); + private ConfigurationValidator buildValidator(DistributionServiceConfig distributionServiceConfig) { + ApplicationVersionConfiguration appConfig = applicationConfigurationPublicationConfig + .buildApplicationVersionConfiguration(distributionServiceConfig); + return new ApplicationVersionConfigurationValidator(appConfig); + } + + @Autowired + DistributionServiceConfig distributionServiceConfig; + + @Autowired + ApplicationConfigurationPublicationConfig applicationConfigurationPublicationConfig; + + @ParameterizedTest + @MethodSource("setSemanticVersionsLatestHigherThanMin") + void succeedsIfLatestHigherThanMin(String latestAndroid, String minAndroid, String latestIos, String minIos) { + distributionServiceConfig.getAppVersions().setLatestAndroid(latestAndroid); + distributionServiceConfig.getAppVersions().setMinAndroid(minAndroid); + distributionServiceConfig.getAppVersions().setLatestIos(latestIos); + distributionServiceConfig.getAppVersions().setMinIos(minIos); + + var validator = buildValidator(distributionServiceConfig); assertThat(validator.validate()).isEqualTo(SUCCESS); } + private static Stream setSemanticVersionsLatestHigherThanMin() { + return Stream.of( + Arguments.of("2.0.0", "1.0.0", "1.0.0", "1.0.0"), + Arguments.of("0.2.0", "0.1.0", "1.0.0", "1.0.0"), + Arguments.of("0.0.2", "0.0.1", "1.0.0", "1.0.0"), + Arguments.of("1.0.0", "1.0.0", "2.0.0", "1.0.0"), + Arguments.of("1.0.0", "1.0.0", "0.2.0", "0.1.0"), + Arguments.of("1.0.0", "1.0.0", "0.0.2", "0.0.1") + ); + } + + @ParameterizedTest + @MethodSource("setSemanticVersionsLatestEqualsMin") + void succeedsWithEqualSemanticVersion(String latestAndroid, String minAndroid, String latestIos, String minIos) { - @Test - void succeedsIfLatestHigherThanMin() throws UnableToLoadFileException { - var validator = buildValidator("app-version/latest-higher-than-min.yaml"); + distributionServiceConfig.getAppVersions().setLatestAndroid(latestAndroid); + distributionServiceConfig.getAppVersions().setMinAndroid(minAndroid); + distributionServiceConfig.getAppVersions().setLatestIos(latestIos); + distributionServiceConfig.getAppVersions().setMinIos(minIos); + + var validator = buildValidator(distributionServiceConfig); assertThat(validator.validate()).isEqualTo(SUCCESS); } - @Test - void failsIfLatestLowerThanMin() throws UnableToLoadFileException { - var validator = buildValidator("app-version/latest-lower-than-min.yaml"); - assertThat(validator.validate()).isEqualTo( - buildExpectedResult(buildError(CONFIG_PREFIX + "ios.[latest|min]", "1.2.2", ErrorType.MIN_GREATER_THAN_MAX))); + private static Stream setSemanticVersionsLatestEqualsMin() { + return Stream.of( + Arguments.of("1.0.0", "1.0.0", "1.0.0", "1.0.0"), + Arguments.of("0.1.0", "0.1.0", "1.0.0", "1.0.0"), + Arguments.of("0.0.1", "0.0.1", "1.0.0", "1.0.0"), + Arguments.of("1.0.0", "1.0.0", "1.0.0", "1.0.0"), + Arguments.of("1.0.0", "1.0.0", "0.1.0", "0.1.0"), + Arguments.of("1.0.0", "1.0.0", "0.0.1", "0.0.1") + ); + } + + @ParameterizedTest + @MethodSource("setSemanticVersionsLatestLowerThanMinAndroid") + void failsWithBadSemanticVersionAndroid(String latestAndroid, String minAndroid, String latestIos, String minIos) { + + distributionServiceConfig.getAppVersions().setLatestAndroid(latestAndroid); + distributionServiceConfig.getAppVersions().setMinAndroid(minAndroid); + distributionServiceConfig.getAppVersions().setLatestIos(latestIos); + distributionServiceConfig.getAppVersions().setMinIos(minIos); + + var validator = buildValidator(distributionServiceConfig); + + assertThat(validator.validate()).isEqualTo(buildExpectedResult(buildError(CONFIG_PREFIX + "android.[latest|min]", minAndroid, ErrorType.MIN_GREATER_THAN_MAX))); + } + private static Stream setSemanticVersionsLatestLowerThanMinAndroid() { + return Stream.of( + Arguments.of("1.0.0", "2.0.0", "1.0.0", "1.0.0"), + Arguments.of("1.0.0", "1.1.0", "1.0.0", "1.0.0"), + Arguments.of("1.0.0", "1.0.1", "1.0.0", "1.0.0") + ); + } + + @ParameterizedTest + @MethodSource("setSemanticVersionsLatestLowerThanMinIos") + void failsWithBadSemanticVersionIos(String latestAndroid, String minAndroid, String latestIos, String minIos) { + + distributionServiceConfig.getAppVersions().setLatestAndroid(latestAndroid); + distributionServiceConfig.getAppVersions().setMinAndroid(minAndroid); + distributionServiceConfig.getAppVersions().setLatestIos(latestIos); + distributionServiceConfig.getAppVersions().setMinIos(minIos); + + var validator = buildValidator(distributionServiceConfig); + + assertThat(validator.validate()).isEqualTo(buildExpectedResult(buildError(CONFIG_PREFIX + "ios.[latest|min]", minIos, ErrorType.MIN_GREATER_THAN_MAX))); } + private static Stream setSemanticVersionsLatestLowerThanMinIos() { + return Stream.of( - private ConfigurationValidator buildValidator(String filePath) throws UnableToLoadFileException { - var configBuilder = YamlLoader.loadYamlIntoProtobufBuilder(filePath, ApplicationVersionConfiguration.Builder.class); - return new ApplicationVersionConfigurationValidator(configBuilder.build()); + Arguments.of("1.0.0", "1.0.0", "1.0.0", "2.0.0"), + Arguments.of("1.0.0", "1.0.0", "1.0.0", "1.1.0"), + Arguments.of("1.0.0", "1.0.0", "1.0.0", "1.0.1") + ); } } diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/integration/ObjectStoreFilePreservationIT.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/integration/ObjectStoreFilePreservationIT.java index e119a66e46..28a0cce2c5 100644 --- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/integration/ObjectStoreFilePreservationIT.java +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/integration/ObjectStoreFilePreservationIT.java @@ -132,7 +132,7 @@ void files_once_published_to_objectstore_should_not_be_overriden_because_of_rete triggerRetentionPolicy(testStartDate); - // Trigger second distrubution after data retention policies were applied + // Trigger second distribution after data retention policies were applied assembleAndDistribute(testOutputFolder.newFolder("output-after-retention")); List filesAfterRetention = getPublishedFiles(); diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/runner/TestDataGenerationTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/runner/TestDataGenerationTest.java index 2ebb7b0275..6e8f1a49bc 100644 --- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/runner/TestDataGenerationTest.java +++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/runner/TestDataGenerationTest.java @@ -26,9 +26,8 @@ import app.coronawarn.server.services.distribution.config.DistributionServiceConfig; import app.coronawarn.server.services.distribution.config.DistributionServiceConfig.TestData; import org.junit.Assert; -import org.junit.Ignore; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -86,6 +85,11 @@ void setup() { testDataGeneration = new TestDataGeneration(diagnosisKeyService, distributionServiceConfig); } + @AfterEach + void tearDown() { + TimeUtils.setNow(null); + } + @Test void shouldCreateKeysAllKeys() { var now = LocalDateTime.of(2020, 7, 15, 12, 0, 0).toInstant(ZoneOffset.UTC); diff --git a/services/distribution/src/test/resources/app-version/all_ok.yaml b/services/distribution/src/test/resources/app-version/all_ok.yaml deleted file mode 100644 index c28b2eb2fa..0000000000 --- a/services/distribution/src/test/resources/app-version/all_ok.yaml +++ /dev/null @@ -1,18 +0,0 @@ -ios: - latest: - major: 1 - minor: 5 - patch: 2 - min: - major: 1 - minor: 2 - patch: 2 -android: - latest: - major: 1 - minor: 3 - patch: 2 - min: - major: 1 - minor: 1 - patch: 2 diff --git a/services/distribution/src/test/resources/app-version/broken_syntax.yaml b/services/distribution/src/test/resources/app-version/broken_syntax.yaml deleted file mode 100644 index 5dfab205ee..0000000000 --- a/services/distribution/src/test/resources/app-version/broken_syntax.yaml +++ /dev/null @@ -1,18 +0,0 @@ -ios: - latest: - major: 1 # wrong syntax, indent too high - minor: 5 - patch: 2 - min: - major: 1 - minor: 2 - patch: 2 -android: - latest: - major: 1 - minor: 3 - patch: 2 - min: - major: 1 - minor: 1 - patch: 2 diff --git a/services/distribution/src/test/resources/app-version/empty.yaml b/services/distribution/src/test/resources/app-version/empty.yaml deleted file mode 100644 index ab2fc5dd55..0000000000 --- a/services/distribution/src/test/resources/app-version/empty.yaml +++ /dev/null @@ -1 +0,0 @@ -# empty file \ No newline at end of file diff --git a/services/distribution/src/test/resources/app-version/latest-equals-min.yaml b/services/distribution/src/test/resources/app-version/latest-equals-min.yaml deleted file mode 100644 index cd0acc0aaa..0000000000 --- a/services/distribution/src/test/resources/app-version/latest-equals-min.yaml +++ /dev/null @@ -1,18 +0,0 @@ -ios: - latest: - major: 1 - minor: 2 - patch: 2 - min: - major: 1 - minor: 2 - patch: 2 -android: - latest: - major: 1 - minor: 3 - patch: 2 - min: - major: 1 - minor: 3 - patch: 2 diff --git a/services/distribution/src/test/resources/app-version/latest-higher-than-min.yaml b/services/distribution/src/test/resources/app-version/latest-higher-than-min.yaml deleted file mode 100644 index c43757881b..0000000000 --- a/services/distribution/src/test/resources/app-version/latest-higher-than-min.yaml +++ /dev/null @@ -1,18 +0,0 @@ -ios: - latest: - major: 1 - minor: 2 - patch: 3 - min: - major: 1 - minor: 2 - patch: 2 -android: - latest: - major: 1 - minor: 3 - patch: 2 - min: - major: 1 - minor: 3 - patch: 2 diff --git a/services/distribution/src/test/resources/app-version/latest-lower-than-min.yaml b/services/distribution/src/test/resources/app-version/latest-lower-than-min.yaml deleted file mode 100644 index 95ff915d67..0000000000 --- a/services/distribution/src/test/resources/app-version/latest-lower-than-min.yaml +++ /dev/null @@ -1,18 +0,0 @@ -ios: - latest: - major: 1 - minor: 2 - patch: 1 - min: - major: 1 - minor: 2 - patch: 2 -android: - latest: - major: 1 - minor: 3 - patch: 2 - min: - major: 1 - minor: 3 - patch: 2 diff --git a/services/distribution/src/test/resources/app-version/wrong_file.yaml b/services/distribution/src/test/resources/app-version/wrong_file.yaml deleted file mode 100644 index 27151279a1..0000000000 --- a/services/distribution/src/test/resources/app-version/wrong_file.yaml +++ /dev/null @@ -1,20 +0,0 @@ -ID: com.acme.mta.sample -version: 1.0.1 -modules: - - name: pricing-ui - type: nodejs - path: ./ui - requires: - - name: thedatabase - - - name: pricing-backend - type: java - path: ./backend - provides: - - name: price_opt - properties: - protocol: http - uri: myhost.mydomain -resources: - - name: thedatabase - type: com.sap.xs.hdi-container \ No newline at end of file diff --git a/services/distribution/src/test/resources/application.yaml b/services/distribution/src/test/resources/application.yaml index a3777192a4..5a9eb6c8b2 100644 --- a/services/distribution/src/test/resources/application.yaml +++ b/services/distribution/src/test/resources/application.yaml @@ -52,6 +52,17 @@ services: max-number-of-failed-operations: 5 max-number-of-s3-threads: 2 force-update-keyfiles: ${FORCE_UPDATE_KEYFILES:false} + + app-features: + - label: isPlausibleDeniabilityActive + value: ${PLAUSIBLE_DENIABILITY_ACTIVE:1} + supported-countries: ${SUPPORTED_COUNTRIES:DE} + app-versions: + latest-ios: ${LATEST_IOS_VERSION:0.8.2} + min-ios: ${MIN_IOS_VERSION:0.5.0} + latest-android: ${LATEST_IOS_VERSION:1.0.4} + min-android: ${MIN_ANDROID_VERSION:1.0.4} + spring: main: banner-mode: off diff --git a/services/distribution/src/test/resources/configtests/app-config_broken_syntax.yaml b/services/distribution/src/test/resources/configtests/app-config_broken_syntax.yaml index 3190f78838..300e3912ce 100644 --- a/services/distribution/src/test/resources/configtests/app-config_broken_syntax.yaml +++ b/services/distribution/src/test/resources/configtests/app-config_broken_syntax.yaml @@ -3,4 +3,3 @@ min-risk-score: attenuation-duration: !include attenuation-duration_ok.yaml risk-score-classes: !include risk-score-class_ok.yaml # illegal indentation exposure-config: !include exposure-config_ok.yaml -app-version: !include app-version-config_ok.yaml diff --git a/services/distribution/src/test/resources/configtests/app-config_ok.yaml b/services/distribution/src/test/resources/configtests/app-config_ok.yaml index be28abc0d8..7465a09cfe 100644 --- a/services/distribution/src/test/resources/configtests/app-config_ok.yaml +++ b/services/distribution/src/test/resources/configtests/app-config_ok.yaml @@ -2,4 +2,3 @@ min-risk-score: 72 attenuation-duration: !include attenuation-duration_ok.yaml risk-score-classes: !include risk-score-class_ok.yaml exposure-config: !include exposure-config_ok.yaml -app-version: !include app-version-config_ok.yaml diff --git a/services/pom.xml b/services/pom.xml index f392e96bf0..b2af4c622b 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -1,5 +1,6 @@ - 4.0.0 diff --git a/services/submission/pom.xml b/services/submission/pom.xml index f8e9dab766..60ff7af724 100644 --- a/services/submission/pom.xml +++ b/services/submission/pom.xml @@ -67,10 +67,21 @@ postgresql test + + com.google.protobuf + protobuf-java-util + 3.12.4 + test + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M3 + org.jacoco jacoco-maven-plugin diff --git a/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java b/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java index b5c8e0d00f..e7d8a20e8c 100644 --- a/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java +++ b/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java @@ -62,6 +62,10 @@ public class SubmissionServiceConfig { private Verification verification; private Monitoring monitoring; private Client client; + @Min(0) + @Max(144) + private Integer maxRollingPeriod; + public Long getInitialFakeDelayMilliseconds() { return initialFakeDelayMilliseconds; @@ -111,6 +115,14 @@ public void setMaximumRequestSize(DataSize maximumRequestSize) { this.maximumRequestSize = maximumRequestSize; } + public Integer getMaxRollingPeriod() { + return maxRollingPeriod; + } + + public void setMaxRollingPeriod(Integer maxRollingPeriod) { + this.maxRollingPeriod = maxRollingPeriod; + } + public Integer getMaxNumberOfKeys() { return payload.getMaxNumberOfKeys(); } @@ -148,7 +160,7 @@ public void setPayload(Payload payload) { private static class Payload { @Min(7) - @Max(28) + @Max(100) private Integer maxNumberOfKeys; @Pattern(regexp = ALLOWED_COUNTRY_CODES_REGEX) diff --git a/services/submission/src/main/java/app/coronawarn/server/services/submission/validation/ValidSubmissionPayload.java b/services/submission/src/main/java/app/coronawarn/server/services/submission/validation/ValidSubmissionPayload.java index 84d685accf..51ee6fce13 100644 --- a/services/submission/src/main/java/app/coronawarn/server/services/submission/validation/ValidSubmissionPayload.java +++ b/services/submission/src/main/java/app/coronawarn/server/services/submission/validation/ValidSubmissionPayload.java @@ -20,8 +20,6 @@ package app.coronawarn.server.services.submission.validation; -import static app.coronawarn.server.common.persistence.domain.DiagnosisKey.EXPECTED_ROLLING_PERIOD; - import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKey; import app.coronawarn.server.common.protocols.internal.SubmissionPayload; import app.coronawarn.server.services.submission.config.SubmissionServiceConfig; @@ -74,11 +72,10 @@ class SubmissionPayloadValidator implements ConstraintValidator { private static final Logger logger = LoggerFactory.getLogger(SubmissionPayloadValidator.class); - - private SubmissionServiceConfig config; + private SubmissionServiceConfig submissionServiceConfig; public SubmissionPayloadValidator(SubmissionServiceConfig submissionServiceConfig) { - this.config = submissionServiceConfig; + this.submissionServiceConfig = submissionServiceConfig; } /** @@ -96,13 +93,18 @@ public boolean isValid(SubmissionPayload submissionPayload, ConstraintValidatorC List exposureKeys = submissionPayload.getKeysList(); validatorContext.disableDefaultConstraintViolation(); - boolean isValid = checkKeyCollectionSize(exposureKeys, validatorContext); - isValid &= checkUniqueStartIntervalNumbers(exposureKeys, validatorContext); - isValid &= checkNoOverlapsInTimeWindow(exposureKeys, validatorContext); - isValid &= checkOriginCountryIsAccepted(submissionPayload, validatorContext); - logIfVisitedCountriesNotAllowed(submissionPayload, validatorContext); - - return isValid; + logIfVisitedCountriesNotAllowed(submissionPayload); + + if (keysHaveFlexibleRollingPeriod(exposureKeys)) { + return checkStartIntervalNumberIsAtMidNight(exposureKeys, validatorContext) + && checkKeysCumulateEqualOrLessThanMaxRollingPeriodPerDay(exposureKeys, validatorContext) + && checkOriginCountryIsAccepted(submissionPayload, validatorContext); + } else { + return checkStartIntervalNumberIsAtMidNight(exposureKeys, validatorContext) + && checkKeyCollectionSize(exposureKeys, validatorContext) + && checkUniqueStartIntervalNumbers(exposureKeys, validatorContext) + && checkOriginCountryIsAccepted(submissionPayload, validatorContext); + } } /** @@ -113,10 +115,8 @@ public boolean isValid(SubmissionPayload submissionPayload, ConstraintValidatorC private boolean checkOriginCountryIsAccepted(SubmissionPayload submissionPayload, ConstraintValidatorContext validatorContext) { - // TODO: Uncomment below when the proto definition is changed - // String originCountry = submissionPayload.getOriginCountry(); - String originCountry = "DE"; - if (!config.isCountryAllowed(originCountry)) { + String originCountry = submissionPayload.getOrigin(); + if (!submissionServiceConfig.isCountryAllowed(originCountry)) { addViolation(validatorContext, String.format( "Origin country %s is not part of the allowed countries list", originCountry)); return false; @@ -128,12 +128,9 @@ private boolean checkOriginCountryIsAccepted(SubmissionPayload submissionPayload * Log a warning if the payload contains a visited country which is not * part of the allowed-countries list. */ - private void logIfVisitedCountriesNotAllowed(SubmissionPayload submissionPayload, - ConstraintValidatorContext validatorContext) { - // TODO: Uncomment below when the proto definition is changed - // List visitedCountries = submissionPayload.getVisitedCountries(); - List visitedCountries = List.of("DE", "FR"); - if (!config.areAllCountriesAllowed(visitedCountries)) { + private void logIfVisitedCountriesNotAllowed(SubmissionPayload submissionPayload) { + List visitedCountries = submissionPayload.getVisitedCountriesList(); + if (!submissionServiceConfig.areAllCountriesAllowed(visitedCountries)) { logger.warn("Submission Payload contains some" + " visited countries which are not allowed: {}", StringUtils.join(visitedCountries, ',')); } @@ -147,14 +144,15 @@ private boolean checkKeyCollectionSize(List exposureKeys, ConstraintValidatorContext validatorContext) { if (exposureKeys.isEmpty() || exceedsMaxNumberOfKeysPerSubmission(exposureKeys)) { addViolation(validatorContext, String.format( - "Number of keys must be between 1 and %s, but is %s.", config.getMaxNumberOfKeys(), exposureKeys.size())); + "Number of keys must be between 1 and %s, but is %s.", + submissionServiceConfig.getMaxNumberOfKeys(), exposureKeys.size())); return false; } return true; } private boolean exceedsMaxNumberOfKeysPerSubmission(List exposureKeys) { - return exposureKeys.size() > config.getMaxNumberOfKeys(); + return exposureKeys.size() > submissionServiceConfig.getMaxNumberOfKeys(); } private boolean checkUniqueStartIntervalNumbers(List exposureKeys, @@ -174,23 +172,37 @@ private boolean checkUniqueStartIntervalNumbers(List expos return true; } - private boolean checkNoOverlapsInTimeWindow(List exposureKeys, + private boolean checkKeysCumulateEqualOrLessThanMaxRollingPeriodPerDay(List exposureKeys, ConstraintValidatorContext validatorContext) { - if (exposureKeys.size() < 2) { - return true; + + boolean isValidRollingPeriod = exposureKeys.stream().collect(Collectors + .groupingBy(TemporaryExposureKey::getRollingStartIntervalNumber, + Collectors.summingInt(TemporaryExposureKey::getRollingPeriod))) + .values().stream() + .anyMatch(sum -> sum <= submissionServiceConfig.getMaxRollingPeriod()); + + if (!isValidRollingPeriod) { + addViolation(validatorContext, "The sum of the rolling periods exceeds 144 per day"); + return false; } + return true; + } + + private boolean keysHaveFlexibleRollingPeriod(List exposureKeys) { + return exposureKeys.stream() + .anyMatch(temporaryExposureKey -> + temporaryExposureKey.getRollingPeriod() < submissionServiceConfig.getMaxRollingPeriod()); + } - Integer[] sortedStartIntervalNumbers = exposureKeys.stream() - .mapToInt(TemporaryExposureKey::getRollingStartIntervalNumber) - .sorted().boxed().toArray(Integer[]::new); - - for (int i = 1; i < sortedStartIntervalNumbers.length; i++) { - if ((sortedStartIntervalNumbers[i - 1] + EXPECTED_ROLLING_PERIOD) > sortedStartIntervalNumbers[i]) { - addViolation(validatorContext, String.format( - "Subsequent intervals overlap. StartIntervalNumbers: %s", - Arrays.stream(sortedStartIntervalNumbers).map(String::valueOf).collect(Collectors.joining(",")))); - return false; - } + private boolean checkStartIntervalNumberIsAtMidNight(List exposureKeys, + ConstraintValidatorContext validatorContext) { + boolean isNotMidNight00Utc = exposureKeys.stream() + .anyMatch(exposureKey -> + exposureKey.getRollingStartIntervalNumber() % submissionServiceConfig.getMaxRollingPeriod() > 0); + + if (isNotMidNight00Utc) { + addViolation(validatorContext, "Start Interval Number must be at midnight ( 00:00 UTC )"); + return false; } return true; } diff --git a/services/submission/src/main/resources/application.yaml b/services/submission/src/main/resources/application.yaml index 027e3cba87..b7cd622d41 100644 --- a/services/submission/src/main/resources/application.yaml +++ b/services/submission/src/main/resources/application.yaml @@ -25,9 +25,11 @@ services: connection-pool-size: 200 # The maximum request size accepted by the SubmissionController (e.g. 200B or 100KB). maximum-request-size: ${MAXIMUM_REQUEST_SIZE:100KB} + # The maximum rolling period accepted by the SubmissionController (e.g. 144 (24 hours)) + max-rolling-period: ${MAX_ROLLING_PERIOD:144} payload: # The maximum number of keys accepted for any submission. - max-number-of-keys: 14 + max-number-of-keys: ${MAX_NUMBER_OF_KEYS:14} # Country codes representing the list of countries which are allowed to be specified in the # visitedCountries and originCountry field of a submission payload # Comma separated country code values must be provided. The value must not end with ',' @@ -122,4 +124,4 @@ feign: config: default: connect-timeout: 5000 - read-timeout: 5000 \ No newline at end of file + read-timeout: 5000 diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/assertions/SubmissionAssertions.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/assertions/SubmissionAssertions.java new file mode 100644 index 0000000000..1a6ff57d97 --- /dev/null +++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/assertions/SubmissionAssertions.java @@ -0,0 +1,67 @@ +/*- + * ---license-start + * Corona-Warn-App + * --- + * Copyright (C) 2020 SAP SE 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 app.coronawarn.server.services.submission.assertions; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import app.coronawarn.server.common.persistence.domain.DiagnosisKey; +import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKey; +import app.coronawarn.server.services.submission.config.SubmissionServiceConfig; + +public final class SubmissionAssertions { + + public static void assertElementsCorrespondToEachOther(Collection submittedTemporaryExposureKeys, + Collection savedDiagnosisKeys, SubmissionServiceConfig config) { + + Set submittedDiagnosisKeys = submittedTemporaryExposureKeys.stream() + .map(submittedDiagnosisKey -> DiagnosisKey.builder().fromTemporaryExposureKey(submittedDiagnosisKey).build()) + .collect(Collectors.toSet()); + + assertThat(savedDiagnosisKeys).hasSize(submittedDiagnosisKeys.size() * config.getRandomKeyPaddingMultiplier()); + assertThat(savedDiagnosisKeys).containsAll(submittedDiagnosisKeys); + + submittedDiagnosisKeys.forEach(submittedDiagnosisKey -> { + List savedKeysForSingleSubmittedKey = savedDiagnosisKeys.stream() + .filter(savedDiagnosisKey -> savedDiagnosisKey.getRollingPeriod() == submittedDiagnosisKey.getRollingPeriod()) + .filter(savedDiagnosisKey -> savedDiagnosisKey.getTransmissionRiskLevel() == submittedDiagnosisKey + .getTransmissionRiskLevel()) + .filter(savedDiagnosisKey -> savedDiagnosisKey.getRollingStartIntervalNumber() == submittedDiagnosisKey + .getRollingStartIntervalNumber()) + .collect(Collectors.toList()); + + assertThat(savedKeysForSingleSubmittedKey).hasSize(config.getRandomKeyPaddingMultiplier()); + assertThat(savedKeysForSingleSubmittedKey.stream() + .filter(savedKey -> Arrays.equals(savedKey.getKeyData(), submittedDiagnosisKey.getKeyData()))).hasSize(1); + assertThat(savedKeysForSingleSubmittedKey) + .allMatch(savedKey -> savedKey.getRollingPeriod() == submittedDiagnosisKey.getRollingPeriod()); + assertThat(savedKeysForSingleSubmittedKey).allMatch(savedKey -> savedKey + .getRollingStartIntervalNumber() == submittedDiagnosisKey.getRollingStartIntervalNumber()); + assertThat(savedKeysForSingleSubmittedKey).allMatch( + savedKey -> savedKey.getTransmissionRiskLevel() == submittedDiagnosisKey.getTransmissionRiskLevel()); + }); + } +} diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/PayloadValidationTest.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/PayloadValidationTest.java index 7470f8fae6..c7dfddb411 100644 --- a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/PayloadValidationTest.java +++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/PayloadValidationTest.java @@ -20,11 +20,7 @@ package app.coronawarn.server.services.submission.controller; -import static app.coronawarn.server.services.submission.controller.RequestExecutor.VALID_KEY_DATA_1; -import static app.coronawarn.server.services.submission.controller.RequestExecutor.VALID_KEY_DATA_2; -import static app.coronawarn.server.services.submission.controller.RequestExecutor.VALID_KEY_DATA_3; -import static app.coronawarn.server.services.submission.controller.RequestExecutor.buildTemporaryExposureKey; -import static app.coronawarn.server.services.submission.controller.RequestExecutor.createRollingStartIntervalNumber; +import static app.coronawarn.server.services.submission.controller.RequestExecutor.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @@ -36,9 +32,13 @@ import app.coronawarn.server.services.submission.verification.TanVerifier; import java.util.ArrayList; import java.util.Collection; +import java.util.List; + import org.assertj.core.util.Lists; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -67,22 +67,21 @@ void check400ResponseStatusForMissingKeys() { } @Test - void check400ResponseStatusForTooManyKeys() { + void check400ResponseStatusForTooManyKeysWithFixedRollingPeriod() { ResponseEntity actResponse = executor.executePost(buildPayloadWithTooManyKeys()); - assertThat(actResponse.getStatusCode()).isEqualTo(BAD_REQUEST); } private Collection buildPayloadWithTooManyKeys() { ArrayList tooMany = new ArrayList<>(); for (int i = 0; i <= 20; i++) { - tooMany.add(buildTemporaryExposureKey(VALID_KEY_DATA_1, createRollingStartIntervalNumber(2), 3)); + tooMany.add(buildTemporaryExposureKey(VALID_KEY_DATA_1, createRollingStartIntervalNumber(2) + i * DiagnosisKey.MAX_ROLLING_PERIOD , 3)); } return tooMany; } @Test - void check400ResponseStatusForDuplicateStartIntervalNumber() { + void check400ResponseStatusForKeysWithFixedRollingPeriodAndDuplicateStartIntervals() { int rollingStartIntervalNumber = createRollingStartIntervalNumber(2); var keysWithDuplicateStartIntervalNumber = Lists.list( buildTemporaryExposureKey(VALID_KEY_DATA_1, rollingStartIntervalNumber, 1), @@ -94,45 +93,127 @@ void check400ResponseStatusForDuplicateStartIntervalNumber() { } @Test - void check200ResponseStatusForGapsInTimeIntervals() { + void check200ResponseStatusForGapsInTimeIntervalsOfKeysWithFixedRollingPeriod() { int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); - int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.EXPECTED_ROLLING_PERIOD; - int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + 2 * DiagnosisKey.EXPECTED_ROLLING_PERIOD; - var keysWithDuplicateStartIntervalNumber = Lists.list( + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.MAX_ROLLING_PERIOD; + int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + 3 * DiagnosisKey.MAX_ROLLING_PERIOD; + var keysWithGapsInStartIntervalNumber = Lists.list( buildTemporaryExposureKey(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1), buildTemporaryExposureKey(VALID_KEY_DATA_3, rollingStartIntervalNumber3, 3), buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2)); - ResponseEntity actResponse = executor.executePost(keysWithDuplicateStartIntervalNumber); + ResponseEntity actResponse = executor.executePost(keysWithGapsInStartIntervalNumber); assertThat(actResponse.getStatusCode()).isEqualTo(OK); } @Test - void check400ResponseStatusForOverlappingTimeIntervals() { + void check200ResponseStatusForGapsInTimeIntervalsOfKeysWithFlexibleRollingPeriod() { int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); - int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + (DiagnosisKey.EXPECTED_ROLLING_PERIOD / 2); - var keysWithDuplicateStartIntervalNumber = Lists.list( - buildTemporaryExposureKey(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1), - buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2)); + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.MAX_ROLLING_PERIOD; + int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + 3 * DiagnosisKey.MAX_ROLLING_PERIOD; + var keysWithGapsInStartIntervalNumber = Lists.list( + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1, 54), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1, 90), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_3, rollingStartIntervalNumber3, 3, 133), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2, 144)); - ResponseEntity actResponse = executor.executePost(keysWithDuplicateStartIntervalNumber); + ResponseEntity actResponse = executor.executePost(keysWithGapsInStartIntervalNumber); + + assertThat(actResponse.getStatusCode()).isEqualTo(OK); + } + @ParameterizedTest + @MethodSource("app.coronawarn.server.services.submission.controller.TEKDatasetGeneration#getOverlappingTestDatasets") + void check400ResponseStatusForOverlappingTimeIntervalsI(List dataset) { + ResponseEntity actResponse = executor.executePost(dataset); assertThat(actResponse.getStatusCode()).isEqualTo(BAD_REQUEST); } + @ParameterizedTest + @MethodSource("app.coronawarn.server.services.submission.controller.TEKDatasetGeneration#getRollingPeriodDatasets") + void check200ResponseStatusForValidSubmissionPayload(List dataset) { + ResponseEntity actResponse = executor.executePost(dataset); + assertThat(actResponse.getStatusCode()).isEqualTo(OK); + } + + /** + * This test generates a payload with keys for the past 30 days. It verifies that validation passes even + * though keys older than application.yml/retention-period would not be stored. + */ @Test - void check200ResponseStatusForValidSubmissionPayload() { - int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); - int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.EXPECTED_ROLLING_PERIOD; - int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + DiagnosisKey.EXPECTED_ROLLING_PERIOD; - var keysWithDuplicateStartIntervalNumber = Lists.list( - buildTemporaryExposureKey(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1), - buildTemporaryExposureKey(VALID_KEY_DATA_3, rollingStartIntervalNumber3, 3), - buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2)); + void check200ResponseStatusForMoreThan14KeysWithValidFlexibleRollingPeriod() { + ResponseEntity actResponse = executor.executePost(buildPayloadWithMoreThan14KeysAndFlexibleRollingPeriod()); + assertThat(actResponse.getStatusCode()).isEqualTo(OK); + } - ResponseEntity actResponse = executor.executePost(keysWithDuplicateStartIntervalNumber); + private Collection buildPayloadWithMoreThan14KeysAndFlexibleRollingPeriod() { + ArrayList flexibleRollingPeriodKeys = new ArrayList<>(); + /* Generate keys with fixed rolling period (144) for the past 20 days */ + for (int i = 0 ; i < 20; i++) { + flexibleRollingPeriodKeys.add(buildTemporaryExposureKey(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(2) - i * DiagnosisKey.MAX_ROLLING_PERIOD, 3)); + } + /* Generate another 10 keys with flexible rolling period (<144) */ + for (int i = 20 ; i < 30; i++) { + flexibleRollingPeriodKeys.add(buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(2) - i * DiagnosisKey.MAX_ROLLING_PERIOD, 3, 133)); + + } + return flexibleRollingPeriodKeys; + } + + @Test + void check400ResponseStatusWhenTwoKeysCumulateMoreThanMaxRollingPeriodInSameDay() { + ResponseEntity actResponse = executor.executePost(buildPayloadWithKeysThatCumulateMoreThanMaxRollingPeriodPerDay()); + assertThat(actResponse.getStatusCode()).isEqualTo(BAD_REQUEST); + } + + private Collection buildPayloadWithKeysThatCumulateMoreThanMaxRollingPeriodPerDay() { + ArrayList temporaryExposureKeys = new ArrayList<>(); + temporaryExposureKeys.add(buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(2), 3, 100)); + temporaryExposureKeys.add(buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(2), 3, 144)); + + return temporaryExposureKeys; + } + + @Test + void check200ResponseStatusWithTwoKeysOneFlexibleAndOneDefaultOnDifferentDays() { + ResponseEntity actResponse = executor.executePost(buildPayloadWithTwoKeysOneFlexibleAndOneDefaultOnDifferentDays()); assertThat(actResponse.getStatusCode()).isEqualTo(OK); } + + private Collection buildPayloadWithTwoKeysOneFlexibleAndOneDefaultOnDifferentDays() { + ArrayList flexibleRollingPeriodKeys = new ArrayList<>(); + + flexibleRollingPeriodKeys.add(buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(2), 3, 100)); + flexibleRollingPeriodKeys.add(buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(3), 3, 144)); + + + return flexibleRollingPeriodKeys; + } + + @Test + void check200ResponseStatusWhenKeysCumulateToMaxRollingPeriodInSameDay() { + ResponseEntity actResponse = executor.executePost(buildPayloadWithTwoKeysWithFlexibleRollingPeriod()); + + assertThat(actResponse.getStatusCode()).isEqualTo(OK); + } + + private Collection buildPayloadWithTwoKeysWithFlexibleRollingPeriod() { + ArrayList flexibleRollingPeriodKeys = new ArrayList<>(); + + flexibleRollingPeriodKeys.add(buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(2), 3, 100)); + flexibleRollingPeriodKeys.add(buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, + createRollingStartIntervalNumber(2), 3, 44)); + + + return flexibleRollingPeriodKeys; + } } diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/RequestExecutor.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/RequestExecutor.java index 32ae7308c8..5306055954 100644 --- a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/RequestExecutor.java +++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/RequestExecutor.java @@ -30,6 +30,7 @@ import java.time.LocalDate; import java.util.Collection; import java.util.Collections; +import java.util.List; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -60,7 +61,10 @@ public ResponseEntity execute(HttpMethod method, RequestEntity executePost(Collection keys, HttpHeaders headers) { - SubmissionPayload body = SubmissionPayload.newBuilder().addAllKeys(keys).build(); + SubmissionPayload body = SubmissionPayload.newBuilder() + .setOrigin("DE") + .addAllVisitedCountries(List.of("FR","UK")) + .addAllKeys(keys).build(); return executePost(body, headers); } @@ -92,6 +96,15 @@ public static TemporaryExposureKey buildTemporaryExposureKey( .setTransmissionRiskLevel(transmissionRiskLevel).build(); } + public static TemporaryExposureKey buildTemporaryExposureKeyWithFlexibleRollingPeriod( + String keyData, int rollingStartIntervalNumber, int transmissionRiskLevel, int rollingPeriod) { + return TemporaryExposureKey.newBuilder() + .setKeyData(ByteString.copyFromUtf8(keyData)) + .setRollingStartIntervalNumber(rollingStartIntervalNumber) + .setTransmissionRiskLevel(transmissionRiskLevel) + .setRollingPeriod(rollingPeriod).build(); + } + public static int createRollingStartIntervalNumber(Integer daysAgo) { return Math.toIntExact(LocalDate .ofInstant(Instant.now(), UTC) @@ -100,6 +113,6 @@ public static int createRollingStartIntervalNumber(Integer daysAgo) { } public static Collection buildPayloadWithOneKey() { - return Collections.singleton(buildTemporaryExposureKey(VALID_KEY_DATA_1, 1, 3)); + return Collections.singleton(buildTemporaryExposureKey(VALID_KEY_DATA_1, createRollingStartIntervalNumber(1), 3)); } } diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionControllerTest.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionControllerTest.java index 26940b7b61..56152cfe9e 100644 --- a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionControllerTest.java +++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionControllerTest.java @@ -26,12 +26,7 @@ import static app.coronawarn.server.services.submission.controller.RequestExecutor.buildPayloadWithOneKey; import static app.coronawarn.server.services.submission.controller.RequestExecutor.buildTemporaryExposureKey; import static app.coronawarn.server.services.submission.controller.RequestExecutor.createRollingStartIntervalNumber; -import static app.coronawarn.server.services.submission.controller.SubmissionPayloadMockData.buildPayload; -import static app.coronawarn.server.services.submission.controller.SubmissionPayloadMockData.buildPayloadWithInvalidKey; -import static app.coronawarn.server.services.submission.controller.SubmissionPayloadMockData.buildPayloadWithInvalidOriginCountry; -import static app.coronawarn.server.services.submission.controller.SubmissionPayloadMockData.buildPayloadWithInvalidVisitedCountries; -import static app.coronawarn.server.services.submission.controller.SubmissionPayloadMockData.buildPayloadWithPadding; -import static app.coronawarn.server.services.submission.controller.SubmissionPayloadMockData.buildPayloadWithTooLargePadding; +import static app.coronawarn.server.services.submission.controller.SubmissionPayloadMockData.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.any; @@ -56,6 +51,7 @@ import app.coronawarn.server.services.submission.monitoring.SubmissionMonitor; import app.coronawarn.server.services.submission.verification.TanVerifier; import com.google.protobuf.ByteString; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -67,6 +63,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -82,6 +80,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles({"disable-ssl-client-verification", "disable-ssl-client-verification-verify-hostname"}) +@TestInstance(Lifecycle.PER_CLASS) class SubmissionControllerTest { @MockBean @@ -136,7 +135,7 @@ void checkResponseStatusForValidParametersWithPadding() { } @Test - void check400ResponseStatusForInvalidParameters() { + void check400ResponseStatusForInvalidKeys() { ResponseEntity actResponse = executor.executePost(buildPayloadWithInvalidKey()); assertThat(actResponse.getStatusCode()).isEqualTo(BAD_REQUEST); } @@ -261,7 +260,6 @@ void testInvalidPaddingSubmissionPayload() { } @Test - @Disabled("Enable this once submission payload proto is defined") void testInvalidOriginCountrySubmissionPayload() { ResponseEntity actResponse = executor.executePost(buildPayloadWithInvalidOriginCountry()); assertThat(actResponse.getStatusCode()).isEqualTo(BAD_REQUEST); @@ -299,8 +297,8 @@ void checkInvalidTanHandlingIsMonitored() { private Collection buildMultipleKeys() { int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(config.getRetentionDays() - 1); - int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.EXPECTED_ROLLING_PERIOD; - int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + DiagnosisKey.EXPECTED_ROLLING_PERIOD; + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.MAX_ROLLING_PERIOD; + int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + DiagnosisKey.MAX_ROLLING_PERIOD; return Stream.of( buildTemporaryExposureKey(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 3), buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber3, 6), @@ -312,7 +310,7 @@ private TemporaryExposureKey createOutdatedKey() { return TemporaryExposureKey.newBuilder() .setKeyData(ByteString.copyFromUtf8(VALID_KEY_DATA_2)) .setRollingStartIntervalNumber(createRollingStartIntervalNumber(config.getRetentionDays())) - .setRollingPeriod(DiagnosisKey.EXPECTED_ROLLING_PERIOD) + .setRollingPeriod(DiagnosisKey.MAX_ROLLING_PERIOD) .setTransmissionRiskLevel(5).build(); } diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionPayloadMockData.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionPayloadMockData.java index e822e64670..624d0b27ba 100644 --- a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionPayloadMockData.java +++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/SubmissionPayloadMockData.java @@ -31,6 +31,7 @@ import com.google.protobuf.ByteString; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,16 +45,33 @@ public static SubmissionPayload buildPayload(TemporaryExposureKey key) { public static SubmissionPayload buildPayload(Collection keys) { return SubmissionPayload.newBuilder() .addAllKeys(keys) + .addAllVisitedCountries(List.of("FR","UK")) + .setOrigin("DE") .build(); } public static SubmissionPayload buildPayload(Collection keys, boolean consentToFederation) { return SubmissionPayload.newBuilder() .addAllKeys(keys) + .addAllVisitedCountries(List.of("FR","UK")) + .setOrigin("DE") .setConsentToFederation(consentToFederation) .build(); } + public static SubmissionPayload buildInvalidPayload(TemporaryExposureKey key) { + Collection keys = Stream.of(key).collect(Collectors.toCollection(ArrayList::new)); + return buildInvalidPayload(keys); + } + + public static SubmissionPayload buildInvalidPayload(Collection keys) { + return SubmissionPayload.newBuilder() + .addAllKeys(keys) + .addAllVisitedCountries(List.of("FR","UK")) + .setOrigin("DE3") + .build(); + } + public static SubmissionPayload buildPayloadWithPadding(Collection keys) { return buildPayloadWithPadding(keys, "PaddingString".getBytes()); } @@ -68,6 +86,8 @@ public static SubmissionPayload buildPayloadWithTooLargePadding(SubmissionServic private static SubmissionPayload buildPayloadWithPadding(Collection keys, byte[] bytes) { return SubmissionPayload.newBuilder() .addAllKeys(keys) + .addAllVisitedCountries(List.of("FR","UK")) + .setOrigin("DE") .setPadding(ByteString.copyFrom(bytes)) .build(); } @@ -79,8 +99,9 @@ public static SubmissionPayload buildPayloadWithInvalidKey() { } public static SubmissionPayload buildPayloadWithInvalidOriginCountry() { - //TODO Implement this once submission payload proto is defined - return null; + TemporaryExposureKey key = + buildTemporaryExposureKey(VALID_KEY_DATA_1, createRollingStartIntervalNumber(2), 2); + return buildInvalidPayload(key); } public static SubmissionPayload buildPayloadWithInvalidVisitedCountries() { diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/TEKDatasetGeneration.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/TEKDatasetGeneration.java new file mode 100644 index 0000000000..7be4a19b9e --- /dev/null +++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/controller/TEKDatasetGeneration.java @@ -0,0 +1,113 @@ +/*- + * ---license-start + * Corona-Warn-App + * --- + * Copyright (C) 2020 SAP SE 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 app.coronawarn.server.services.submission.controller; + +import app.coronawarn.server.common.persistence.domain.DiagnosisKey; +import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKey; +import org.assertj.core.util.Lists; +import org.junit.jupiter.params.provider.Arguments; +import java.util.List; +import java.util.stream.Stream; + +import static app.coronawarn.server.services.submission.controller.RequestExecutor.*; +import static app.coronawarn.server.services.submission.controller.RequestExecutor.VALID_KEY_DATA_2; + +public class TEKDatasetGeneration { + + private static Stream getOverlappingTestDatasets() { + return Stream.of( + getOverlappingDatasetWithFixedPeriods(), + getOverlappingDatasetWithFlexiblePeriods(), + getMixedOverlappingDataset() + ).map(Arguments::of); + } + + private static List getOverlappingDatasetWithFixedPeriods(){ + int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + (DiagnosisKey.MAX_ROLLING_PERIOD / 2); + return Lists.list( + buildTemporaryExposureKey(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1), + buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2)); + + } + + private static List getOverlappingDatasetWithFlexiblePeriods(){ + int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + (DiagnosisKey.MAX_ROLLING_PERIOD / 2); + return Lists.list( + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1, 54), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1, 90), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2, 133)); + + } + + private static List getMixedOverlappingDataset(){ + int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + (DiagnosisKey.MAX_ROLLING_PERIOD / 2); + return Lists.list( + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1, 54), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1, 1, 90), + buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2)); + + } + + private static Stream getRollingPeriodDatasets() { + return Stream.of( + getFixedRollingPeriodDataset(), + getFlexibleRollingPeriodDataset(), + getMixedRollingPeriodDataset() + ).map(Arguments::of); + } + + private static List getFixedRollingPeriodDataset(){ + int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.MAX_ROLLING_PERIOD; + int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + DiagnosisKey.MAX_ROLLING_PERIOD; + return Lists.list( + buildTemporaryExposureKey(VALID_KEY_DATA_3, rollingStartIntervalNumber1, 3), + buildTemporaryExposureKey(VALID_KEY_DATA_3, rollingStartIntervalNumber3, 3), + buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2)); + } + + private static List getFlexibleRollingPeriodDataset(){ + int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.MAX_ROLLING_PERIOD; + int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + DiagnosisKey.MAX_ROLLING_PERIOD; + return Lists.list( + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1,3, 54), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1,3, 90), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_3, rollingStartIntervalNumber3,3, 133), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_3, rollingStartIntervalNumber3,2, 11), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_2, rollingStartIntervalNumber2,2, 100)); + } + + private static List getMixedRollingPeriodDataset(){ + int rollingStartIntervalNumber1 = createRollingStartIntervalNumber(6); + int rollingStartIntervalNumber2 = rollingStartIntervalNumber1 + DiagnosisKey.MAX_ROLLING_PERIOD; + int rollingStartIntervalNumber3 = rollingStartIntervalNumber2 + DiagnosisKey.MAX_ROLLING_PERIOD; + return Lists.list( + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1,3, 54), + buildTemporaryExposureKeyWithFlexibleRollingPeriod(VALID_KEY_DATA_1, rollingStartIntervalNumber1,3, 90), + buildTemporaryExposureKey(VALID_KEY_DATA_3, rollingStartIntervalNumber3, 3), + buildTemporaryExposureKey(VALID_KEY_DATA_2, rollingStartIntervalNumber2, 2)); + } + +} diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/integration/SubmissionPersistenceIT.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/integration/SubmissionPersistenceIT.java new file mode 100644 index 0000000000..65527f87fd --- /dev/null +++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/integration/SubmissionPersistenceIT.java @@ -0,0 +1,131 @@ +/*- + * ---license-start + * Corona-Warn-App + * --- + * Copyright (C) 2020 SAP SE 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 app.coronawarn.server.services.submission.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static app.coronawarn.server.services.submission.assertions.SubmissionAssertions.*; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.tomcat.util.buf.StringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import com.google.common.io.BaseEncoding; +import com.google.protobuf.util.JsonFormat; +import app.coronawarn.server.common.persistence.service.DiagnosisKeyService; +import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKey; +import app.coronawarn.server.common.protocols.internal.SubmissionPayload; +import app.coronawarn.server.services.submission.config.SubmissionServiceConfig; +import app.coronawarn.server.services.submission.controller.FakeDelayManager; +import app.coronawarn.server.services.submission.controller.RequestExecutor; +import app.coronawarn.server.services.submission.verification.TanVerifier; + +/** + * This test serves more like a dev tool which helps with debugging production issues. + * It inserts keys parsed from a proto buf file whos content was captured by the mobile + * client during requests to the server. The content of the current test resource file + * can be quickly replaced during the investigation of an issue. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles({ "disable-ssl-client-verification", "disable-ssl-client-verification-verify-hostname" }) +@Sql(scripts = {"classpath:db/clean_db_state.sql"}, + executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) +public class SubmissionPersistenceIT { + + private static final Logger logger = LoggerFactory.getLogger(SubmissionPersistenceIT.class); + + @Autowired + private DiagnosisKeyService diagnosisKeyService; + + @Autowired + private RequestExecutor executor; + + @Autowired + private SubmissionServiceConfig config; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @MockBean + private TanVerifier tanVerifier; + + @MockBean + private FakeDelayManager fakeDelayManager; + + @BeforeEach + public void setUpMocks() { + when(tanVerifier.verifyTan(anyString())).thenReturn(true); + when(fakeDelayManager.getJitteredFakeDelay()).thenReturn(1000L); + } + + @Disabled("Because the content of the .pb file becomes old and retention time passes, this test will fail. " + + "Enable when debugging of a new payload is required.") + @ParameterizedTest + @ValueSource(strings = { "src/test/resources/payload/mobile-client-payload.pb" }) + public void testKeyInsertionWithMobileClientProtoBuf(String testFile) throws IOException { + Path path = Paths.get(testFile); + InputStream input = new FileInputStream(path.toFile()); + SubmissionPayload payload = SubmissionPayload.parseFrom(input); + + logger.info("Submitting payload: " + System.lineSeparator() + + JsonFormat.printer().preservingProtoFieldNames().omittingInsignificantWhitespace().print(payload)); + + executor.executePost(payload); + + String presenceVerificationSql = generateDebugSqlStatement(payload); + logger.info("SQL debugging statement: " + System.lineSeparator() + presenceVerificationSql); + Integer result = jdbcTemplate.queryForObject(presenceVerificationSql, Integer.class); + + assertEquals(payload.getKeysList().size(), result); + assertElementsCorrespondToEachOther(payload.getKeysList(), diagnosisKeyService.getDiagnosisKeys(), config); + } + + private String generateDebugSqlStatement(SubmissionPayload payload) { + List base64Keys = payload.getKeysList() + .stream() + .map(key -> "'" + toBase64(key) + "'") + .collect(Collectors.toList()); + return "SELECT count(*) FROM diagnosis_key where ENCODE(key_data, 'BASE64') IN (" + + StringUtils.join(base64Keys, ',') + ")"; + } + + private String toBase64(TemporaryExposureKey key) { + return BaseEncoding.base64().encode((key.getKeyData()).toByteArray()); + } +} diff --git a/services/submission/src/test/resources/application.yaml b/services/submission/src/test/resources/application.yaml index 2b40852c56..cdbae27ae4 100644 --- a/services/submission/src/test/resources/application.yaml +++ b/services/submission/src/test/resources/application.yaml @@ -4,6 +4,12 @@ logging: org: springframework: off root: off + app: + coronawarn: + server: + services: + submission: + integration: info spring: main: banner-mode: off @@ -20,6 +26,7 @@ services: random-key-padding-multiplier: 10 connection-pool-size: 200 maximum-request-size: 100KB + max-rolling-period: 144 payload: max-number-of-keys: 14 allowed-countries: DE,FR,UK,DK @@ -41,6 +48,8 @@ management: exposure: include: 'health' health: + livenessstate: + enabled: true probes: enabled: true diff --git a/services/submission/src/test/resources/db/clean_db_state.sql b/services/submission/src/test/resources/db/clean_db_state.sql new file mode 100644 index 0000000000..95791a1f22 --- /dev/null +++ b/services/submission/src/test/resources/db/clean_db_state.sql @@ -0,0 +1 @@ +truncate table diagnosis_key; \ No newline at end of file diff --git a/services/submission/src/test/resources/payload/mobile-client-payload.pb b/services/submission/src/test/resources/payload/mobile-client-payload.pb new file mode 100644 index 0000000000..e1fe3efd37 Binary files /dev/null and b/services/submission/src/test/resources/payload/mobile-client-payload.pb differ