diff --git a/pom.xml b/pom.xml
index cbe3846a..94aa2ef9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
app.coronawarn
cwa-parent
- 1.8
+ 1.8.1
cwa-quick-test-backend
1.0.0-SNAPSHOT
@@ -139,6 +139,12 @@
eu.europa.ec.dgc
dgc-lib
+
+ net.lingala.zip4j
+ zip4j
+
+
+
@@ -158,6 +164,7 @@
org.codehaus.mojo
license-maven-plugin
+ 2.0.0
diff --git a/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java b/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java
index b8aeae92..41d1811c 100644
--- a/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java
+++ b/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java
@@ -52,6 +52,8 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
public static final String ROLE_POC_NAT_ADMIN = "ROLE_c19_quick_test_poc_nat_admin";
public static final String ROLE_TERMINATOR = "ROLE_c19_quick_test_terminator";
public static final String ROLE_ARCHIVE_OPERATOR = "ROLE_c19_quick_test_archive_operator";
+ public static final String ROLE_ARCHIVE_ZIP_CREATOR = "ROLE_c19_quick_test_archive_operator_zip_creator";
+ public static final String ROLE_ARCHIVE_ZIP_DOWNLOADER = "ROLE_c19_quick_test_archive_operator_zip_downloader";
private static final String API_ROUTE = "/api/**";
private static final String CONFIG_ROUTE = "/api/config/*";
diff --git a/src/main/java/app/coronawarn/quicktest/controller/ArchiveExportController.java b/src/main/java/app/coronawarn/quicktest/controller/ArchiveExportController.java
index c7a2dcfc..63b94e5e 100644
--- a/src/main/java/app/coronawarn/quicktest/controller/ArchiveExportController.java
+++ b/src/main/java/app/coronawarn/quicktest/controller/ArchiveExportController.java
@@ -21,8 +21,12 @@
package app.coronawarn.quicktest.controller;
import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_OPERATOR;
+import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_ZIP_CREATOR;
+import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_ZIP_DOWNLOADER;
+import app.coronawarn.quicktest.model.cancellation.ZipRequest;
import app.coronawarn.quicktest.service.ArchiveService;
+import app.coronawarn.quicktest.service.ArchiveZipService;
import com.opencsv.exceptions.CsvDataTypeMismatchException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import io.swagger.v3.oas.annotations.Operation;
@@ -30,6 +34,14 @@
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import java.io.IOException;
+import java.net.URL;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+import javax.validation.Valid;
+import javax.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
@@ -39,9 +51,13 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.Authentication;
+import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@@ -51,10 +67,15 @@
@RequestMapping(value = "/api/archive")
@RequiredArgsConstructor
@Profile("archive_export")
+@Validated
public class ArchiveExportController {
private final ArchiveService archiveService;
+ private final ArchiveZipService archiveZipService;
+
+ private static final String zipFileNameRegex = "^\\w{0,20}.zip$";
+
/**
* Endpoint for downloading archived entities.
*
@@ -65,14 +86,14 @@ public class ArchiveExportController {
description = "Creates a CSV-File with all archived data for whole Partner or just one POC ID.",
parameters = {
@Parameter(
- in = ParameterIn.PATH,
- name = "partnerId",
- description = "Partner ID of the PArtner to download data of",
- required = true),
+ in = ParameterIn.PATH,
+ name = "partnerId",
+ description = "Partner ID of the PArtner to download data of",
+ required = true),
@Parameter(
- in = ParameterIn.QUERY,
- name = "pocId",
- description = "Filter for entities with given pocId")
+ in = ParameterIn.QUERY,
+ name = "pocId",
+ description = "Filter for entities with given pocId")
}
)
@ApiResponses(value = {
@@ -101,4 +122,80 @@ public ResponseEntity exportArchive(@PathVariable("partnerId") String pa
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create CSV.");
}
}
+
+ /**
+ * Endpoint for creating a zip file with multiple CSV-files.
+ *
+ * @return Status Code.
+ */
+ @Operation(
+ summary = "Create Archive ZIP-File",
+ description = "Creates a ZIP-File with all CSV-Files of provided partner ids."
+ + "ZIP File will be stored in OBS bucket"
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "ZIP Created")
+ })
+ @PostMapping(value = "/zip", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ @Secured({ROLE_ARCHIVE_ZIP_CREATOR})
+ public ResponseEntity createZip(@Valid @RequestBody ZipRequest zipRequest) {
+
+ try {
+ List partnerIds = zipRequest.getPartnerIds();
+ partnerIds.add(zipRequest.getPartnerId());
+
+ archiveZipService.createZip(
+ zipRequest.getPartnerId() + ".zip",
+ zipRequest.getPartnerIds(),
+ zipRequest.getPassword());
+
+ } catch (ArchiveZipService.ArchiveZipServiceException e) {
+ if (e.getReason() == ArchiveZipService.ArchiveZipServiceException.Reason.CSV_FILE_NOT_FOUND) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "CSV File " + e.getMessage() + " not found");
+ } else {
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
+ "Unexpected Error while creating ZIP File.");
+ }
+ } catch (IOException e) {
+ log.error("Failed to create ZIP (IOException): {}", e.getMessage());
+ throw new RuntimeException(e);
+ }
+
+ return ResponseEntity
+ .status(HttpStatus.CREATED)
+ .build();
+ }
+
+ /**
+ * Endpoint for receiving download link for Archive ZIP File.
+ *
+ * @return Download link of zip
+ */
+ @Operation(
+ summary = "Request Download Link (ZIP)",
+ description = "Returns a presigned URL to download the generated ZIP File from Bucket."
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Successful"),
+ @ApiResponse(responseCode = "404", description = "No ZIP file found")
+ })
+ @GetMapping(value = "/zip/download", produces = MediaType.APPLICATION_JSON_VALUE)
+ @Secured({ROLE_ARCHIVE_ZIP_DOWNLOADER})
+ public ResponseEntity getZipLink(
+ @Valid @Pattern(regexp = zipFileNameRegex) @RequestParam("filename") String filename) {
+
+ Date expiration = Date.from(Instant.now().plus(5, ChronoUnit.MINUTES));
+ URL url;
+ try {
+ url = archiveZipService.getDownloadUrl(filename, expiration);
+ } catch (ArchiveZipService.ArchiveZipServiceException e) {
+ if (e.getReason() == ArchiveZipService.ArchiveZipServiceException.Reason.ZIP_FILE_NOT_FOUND) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "ZIP File doesn't exist in Bucket");
+ } else {
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ }
+
+ return ResponseEntity.ok(url);
+ }
}
diff --git a/src/main/java/app/coronawarn/quicktest/model/cancellation/ZipRequest.java b/src/main/java/app/coronawarn/quicktest/model/cancellation/ZipRequest.java
new file mode 100644
index 00000000..a1f1b3a6
--- /dev/null
+++ b/src/main/java/app/coronawarn/quicktest/model/cancellation/ZipRequest.java
@@ -0,0 +1,44 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App / cwa-quick-test-backend
+ * ---
+ * Copyright (C) 2021 - 2023 T-Systems International GmbH and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package app.coronawarn.quicktest.model.cancellation;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import javax.validation.constraints.Size;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+@Schema(
+ description = "The ZIP request model to create zip file for partners."
+)
+@Data
+public class ZipRequest {
+
+ @Length(min = 10, max = 100)
+ private String password;
+
+ @Length(min = 1, max = 50)
+ private String partnerId;
+
+ @Size(min = 1, max = 20)
+ private List<@Length(min = 1, max = 100) String> partnerIds;
+
+}
diff --git a/src/main/java/app/coronawarn/quicktest/service/ArchiveZipService.java b/src/main/java/app/coronawarn/quicktest/service/ArchiveZipService.java
new file mode 100644
index 00000000..337c2133
--- /dev/null
+++ b/src/main/java/app/coronawarn/quicktest/service/ArchiveZipService.java
@@ -0,0 +1,172 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App / cwa-quick-test-backend
+ * ---
+ * Copyright (C) 2021 - 2023 T-Systems International GmbH and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package app.coronawarn.quicktest.service;
+
+import app.coronawarn.quicktest.config.CsvUploadConfig;
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.HttpMethod;
+import com.amazonaws.SdkClientException;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+import com.amazonaws.services.s3.model.S3Object;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.lingala.zip4j.io.outputstream.ZipOutputStream;
+import net.lingala.zip4j.model.ZipParameters;
+import net.lingala.zip4j.model.enums.AesKeyStrength;
+import net.lingala.zip4j.model.enums.AesVersion;
+import net.lingala.zip4j.model.enums.CompressionMethod;
+import net.lingala.zip4j.model.enums.EncryptionMethod;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ArchiveZipService {
+
+ private final CsvUploadConfig csvConfig;
+
+ private final AmazonS3 s3Client;
+
+ /**
+ * Create an encrypted ZIP file in OBS Bucket for given partnerIds.
+ *
+ * @param filename Filename of the ZIP file to be created in Bucket
+ * @param partnerIds List of the PartnerIds to be added to ZIP file.
+ * @param password password to encrypt ZIP file
+ */
+ public void createZip(String filename, List partnerIds, String password)
+ throws ArchiveZipServiceException, IOException {
+
+
+ ZipParameters zipParameters = new ZipParameters();
+ zipParameters.setEncryptFiles(true);
+ zipParameters.setEncryptionMethod(EncryptionMethod.AES);
+ zipParameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
+ zipParameters.setAesVersion(AesVersion.TWO);
+ zipParameters.setCompressionMethod(CompressionMethod.DEFLATE);
+
+ try (
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ ZipOutputStream zipStream = new ZipOutputStream(byteArrayOutputStream, password.toCharArray())) {
+ for (String partnerId : partnerIds) {
+ String csvFileName = partnerId + ".csv";
+ S3Object s3Object;
+
+ try {
+ s3Object = s3Client.getObject(csvConfig.getBucketName(), csvFileName);
+ } catch (AmazonServiceException e) {
+ if (e.getStatusCode() == 404) {
+ log.error("Could not find {} in bucket.", csvFileName);
+ throw new ArchiveZipServiceException(csvFileName,
+ ArchiveZipServiceException.Reason.CSV_FILE_NOT_FOUND);
+ } else {
+ log.error("Unexpected error when downloading CSV {} from bucket. Status Code: {}",
+ csvFileName, e.getStatusCode());
+ throw new ArchiveZipServiceException(csvFileName,
+ ArchiveZipServiceException.Reason.UNEXPECTED_ERROR);
+ }
+ }
+
+ byte[] buff = new byte[4096];
+ int readLen;
+
+ zipParameters.setFileNameInZip(csvFileName);
+ zipStream.putNextEntry(zipParameters);
+ try (InputStream inputStream = s3Object.getObjectContent()) {
+ while ((readLen = inputStream.read(buff)) != -1) {
+ zipStream.write(buff, 0, readLen);
+ }
+ }
+ zipStream.closeEntry();
+ }
+ zipStream.close();
+
+ byte[] zipBytes = byteArrayOutputStream.toByteArray();
+ ObjectMetadata objectMetadata = new ObjectMetadata();
+ objectMetadata.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ objectMetadata.setContentLength(zipBytes.length);
+ s3Client.putObject(
+ csvConfig.getBucketName(),
+ filename,
+ new ByteArrayInputStream(zipBytes),
+ objectMetadata);
+ } catch (ArchiveZipServiceException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("Unexpected error while creating ZIP file.", e);
+ throw new ArchiveZipServiceException(e.getMessage(), ArchiveZipServiceException.Reason.UNEXPECTED_ERROR);
+ }
+ }
+
+ /**
+ * Create a presigned URL to download a file from OBS bucket.
+ *
+ * @param filename name of the file in bucket.
+ * @return URL to download file.
+ * @throws ArchiveZipServiceException if creation went wrong.
+ */
+ public URL getDownloadUrl(String filename, Date expiration) throws ArchiveZipServiceException {
+
+ if (!s3Client.doesObjectExist(csvConfig.getBucketName(), filename)) {
+ throw new ArchiveZipServiceException(filename, ArchiveZipServiceException.Reason.ZIP_FILE_NOT_FOUND);
+ }
+
+ GeneratePresignedUrlRequest generatePresignedUrlRequest =
+ new GeneratePresignedUrlRequest(csvConfig.getBucketName(), filename)
+ .withMethod(HttpMethod.GET)
+ .withExpiration(expiration);
+
+ URL presignedUrl;
+ try {
+ presignedUrl = s3Client.generatePresignedUrl(generatePresignedUrlRequest);
+ } catch (SdkClientException e) {
+ throw new ArchiveZipServiceException(e.getMessage(), ArchiveZipServiceException.Reason.UNEXPECTED_ERROR);
+ }
+
+ return presignedUrl;
+ }
+
+ @RequiredArgsConstructor
+ @Getter
+ public static class ArchiveZipServiceException extends Exception {
+
+ private final String message;
+ private final Reason reason;
+
+ public enum Reason {
+ CSV_FILE_NOT_FOUND,
+ ZIP_FILE_NOT_FOUND,
+ UNEXPECTED_ERROR
+ }
+ }
+
+}
diff --git a/src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java b/src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java
index 80144361..c2cae298 100644
--- a/src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java
+++ b/src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java
@@ -21,37 +21,62 @@
package app.coronawarn.quicktest.controller;
import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_OPERATOR;
+import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_ZIP_CREATOR;
+import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_ZIP_DOWNLOADER;
import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_COUNTER;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import app.coronawarn.quicktest.config.CsvUploadConfig;
import app.coronawarn.quicktest.config.QuicktestKeycloakSpringBootConfigResolver;
import app.coronawarn.quicktest.domain.QuickTestArchive;
import app.coronawarn.quicktest.model.Sex;
+import app.coronawarn.quicktest.model.cancellation.ZipRequest;
import app.coronawarn.quicktest.repository.QuickTestArchiveRepository;
import app.coronawarn.quicktest.service.ArchiveSchedulingService;
import app.coronawarn.quicktest.utils.Utilities;
+import com.amazonaws.HttpMethod;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+import com.amazonaws.services.s3.model.S3Object;
import com.c4_soft.springaddons.security.oauth2.test.annotations.OpenIdClaims;
import com.c4_soft.springaddons.security.oauth2.test.annotations.keycloak.WithMockKeycloakAuth;
import com.c4_soft.springaddons.security.oauth2.test.mockmvc.keycloak.ServletKeycloakAuthUnitTestingSupport;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.opencsv.CSVParser;
import com.opencsv.CSVParserBuilder;
import com.opencsv.CSVReader;
import com.opencsv.CSVReaderBuilder;
import com.opencsv.exceptions.CsvException;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.StringReader;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
+import net.lingala.zip4j.io.inputstream.ZipInputStream;
+import net.lingala.zip4j.model.LocalFileHeader;
+import net.lingala.zip4j.model.enums.EncryptionMethod;
import org.apache.commons.lang3.RandomUtils;
import org.apache.tomcat.util.buf.HexUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
+import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
@@ -61,6 +86,7 @@
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.util.StreamUtils;
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = "keycloak-admin.realm=REALM")
@@ -76,6 +102,15 @@ class ArchiveExportControllerTest extends ServletKeycloakAuthUnitTestingSupport
@Autowired
private ArchiveSchedulingService archiveSchedulingService;
+ @MockBean
+ private AmazonS3 s3Client;
+
+ @Autowired
+ CsvUploadConfig csvUploadConfig;
+
+ @Autowired
+ ObjectMapper objectMapper;
+
private final static String userId = "user-id";
public static final String PARTNER_ID_1 = "P10000";
@@ -86,10 +121,12 @@ class ArchiveExportControllerTest extends ServletKeycloakAuthUnitTestingSupport
public static final String POC_ID_2 = "Poc_43";
+ public static final String ZIP_PASSWORD = "abcdefghijklmnopqrstuvwqyz";
+
@Test
@WithMockKeycloakAuth(
- authorities = ROLE_ARCHIVE_OPERATOR,
- claims = @OpenIdClaims(sub = userId)
+ authorities = ROLE_ARCHIVE_OPERATOR,
+ claims = @OpenIdClaims(sub = userId)
)
void downloadArchiveByPartnerId() throws Exception {
@@ -105,24 +142,24 @@ void downloadArchiveByPartnerId() throws Exception {
archiveSchedulingService.moveToArchiveJob();
MvcResult mvcResult = mockMvc().perform(MockMvcRequestBuilders
- .get("/api/archive/" + PARTNER_ID_1))
- .andReturn();
+ .get("/api/archive/" + PARTNER_ID_1))
+ .andReturn();
Assertions.assertEquals(HttpStatus.OK.value(), mvcResult.getResponse().getStatus());
Assertions.assertEquals(MediaType.APPLICATION_OCTET_STREAM_VALUE, mvcResult.getResponse().getHeader(HttpHeaders.CONTENT_TYPE));
Assertions.assertEquals("attachment; filename=quicktest_export.csv", mvcResult.getResponse().getHeader(HttpHeaders.CONTENT_DISPOSITION));
checkCsv(mvcResult.getResponse().getContentAsByteArray(), 24, 5);
mvcResult = mockMvc().perform(MockMvcRequestBuilders
- .get("/api/archive/" + PARTNER_ID_2))
- .andReturn();
+ .get("/api/archive/" + PARTNER_ID_2))
+ .andReturn();
Assertions.assertEquals(HttpStatus.OK.value(), mvcResult.getResponse().getStatus());
Assertions.assertEquals(MediaType.APPLICATION_OCTET_STREAM_VALUE, mvcResult.getResponse().getHeader(HttpHeaders.CONTENT_TYPE));
Assertions.assertEquals("attachment; filename=quicktest_export.csv", mvcResult.getResponse().getHeader(HttpHeaders.CONTENT_DISPOSITION));
checkCsv(mvcResult.getResponse().getContentAsByteArray(), 24, 4);
mvcResult = mockMvc().perform(MockMvcRequestBuilders
- .get("/api/archive/randomPartnerId"))
- .andReturn();
+ .get("/api/archive/randomPartnerId"))
+ .andReturn();
Assertions.assertEquals(HttpStatus.OK.value(), mvcResult.getResponse().getStatus());
Assertions.assertEquals(MediaType.APPLICATION_OCTET_STREAM_VALUE, mvcResult.getResponse().getHeader(HttpHeaders.CONTENT_TYPE));
Assertions.assertEquals("attachment; filename=quicktest_export.csv", mvcResult.getResponse().getHeader(HttpHeaders.CONTENT_DISPOSITION));
@@ -132,26 +169,139 @@ void downloadArchiveByPartnerId() throws Exception {
@Test
@WithMockKeycloakAuth(
- authorities = ROLE_COUNTER,
- claims = @OpenIdClaims(sub = userId)
+ authorities = ROLE_COUNTER,
+ claims = @OpenIdClaims(sub = userId)
)
void downloadArchiveWrongRole() throws Exception {
mockMvc().perform(MockMvcRequestBuilders
- .get("/api/archive/" + PARTNER_ID_1))
- .andExpect(status().isForbidden());
+ .get("/api/archive/" + PARTNER_ID_1))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @WithMockKeycloakAuth(
+ authorities = ROLE_ARCHIVE_ZIP_CREATOR,
+ claims = @OpenIdClaims(sub = userId)
+ )
+ void testCreateZip() throws Exception {
+ ZipRequest zipRequest = new ZipRequest();
+ zipRequest.setPartnerId(PARTNER_ID_1);
+ zipRequest.setPartnerIds(List.of(PARTNER_ID_2));
+ zipRequest.setPassword(ZIP_PASSWORD);
+
+ byte[] csv1 = RandomUtils.nextBytes(1000);
+ byte[] csv2 = RandomUtils.nextBytes(1000);
+
+ S3Object csv1Object = new S3Object();
+ csv1Object.setObjectContent(new ByteArrayInputStream(csv1));
+
+ S3Object csv2Object = new S3Object();
+ csv2Object.setObjectContent(new ByteArrayInputStream(csv2));
+
+ when(s3Client.getObject(csvUploadConfig.getBucketName(), PARTNER_ID_1 + ".csv"))
+ .thenReturn(csv1Object);
+ when(s3Client.getObject(csvUploadConfig.getBucketName(), PARTNER_ID_2 + ".csv"))
+ .thenReturn(csv2Object);
+
+ ArgumentCaptor zipUploadStreamCaptor = ArgumentCaptor.forClass(InputStream.class);
+ ArgumentCaptor zipUploadMetaCaptor = ArgumentCaptor.forClass(ObjectMetadata.class);
+
+ when(s3Client.putObject(
+ eq(csvUploadConfig.getBucketName()),
+ eq(PARTNER_ID_1 + ".zip"),
+ zipUploadStreamCaptor.capture(),
+ zipUploadMetaCaptor.capture()))
+ .thenReturn(null);
+
+ mockMvc().perform(MockMvcRequestBuilders
+ .post("/api/archive/zip")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsBytes(zipRequest)))
+ .andExpect(status().isCreated())
+ .andExpect(content().bytes(new byte[0]));
+
+ csv1Object.close();
+ csv2Object.close();
+
+ Assertions.assertNotNull(zipUploadMetaCaptor.getValue());
+ Assertions.assertNotNull(zipUploadStreamCaptor.getValue());
+
+ byte[] uploadedZip = StreamUtils.copyToByteArray(zipUploadStreamCaptor.getValue());
+
+ ObjectMetadata uploadedMetadata = zipUploadMetaCaptor.getValue();
+ Assertions.assertEquals(MediaType.APPLICATION_OCTET_STREAM_VALUE, uploadedMetadata.getContentType());
+ Assertions.assertEquals(uploadedZip.length, uploadedMetadata.getContentLength());
+
+ ZipInputStream zipStream = new ZipInputStream(new ByteArrayInputStream(uploadedZip), ZIP_PASSWORD.toCharArray());
+
+ for (byte i = 0; i < 2; i++) {
+ System.out.println(i);
+ LocalFileHeader zipFile = zipStream.getNextEntry();
+ Assertions.assertEquals(EncryptionMethod.AES, zipFile.getEncryptionMethod());
+
+ if (zipFile.getFileName().equals(PARTNER_ID_1 + ".csv")) {
+ Assertions.assertArrayEquals(csv1, zipStream.readAllBytes());
+ } else if (zipFile.getFileName().equals(PARTNER_ID_2 + ".csv")) {
+ Assertions.assertArrayEquals(csv2, zipStream.readAllBytes());
+ } else {
+ Assertions.fail();
+ }
+ }
+
+ Assertions.assertNull(zipStream.getNextEntry());
+
+ zipStream.close();
+ }
+
+ @Test
+ @WithMockKeycloakAuth(
+ authorities = ROLE_ARCHIVE_ZIP_DOWNLOADER,
+ claims = @OpenIdClaims(sub = userId)
+ )
+ void testRequestPresignedUrl() throws Exception {
+ URL presignedUrl = new URL("https://example.org");
+
+ when(s3Client.doesObjectExist(csvUploadConfig.getBucketName(), PARTNER_ID_1 + ".zip"))
+ .thenReturn(true);
+ when(s3Client.doesObjectExist(csvUploadConfig.getBucketName(), PARTNER_ID_2 + ".zip"))
+ .thenReturn(false);
+
+ ArgumentCaptor presignedUrlRequestArgumentCaptor = ArgumentCaptor.forClass(GeneratePresignedUrlRequest.class);
+ when(s3Client.generatePresignedUrl(presignedUrlRequestArgumentCaptor.capture()))
+ .thenReturn(presignedUrl);
+
+ mockMvc().perform(MockMvcRequestBuilders
+ .get("/api/archive/zip/download?filename=" + PARTNER_ID_2 + ".zip"))
+ .andExpect(status().isNotFound())
+ .andExpect(content().bytes(new byte[0]));
+
+ verify(s3Client, never()).generatePresignedUrl(any());
+
+ mockMvc().perform(MockMvcRequestBuilders
+ .get("/api/archive/zip/download?filename=" + PARTNER_ID_1 + ".zip"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(objectMapper.writeValueAsString(presignedUrl)));
+
+ long now = Instant.now().toEpochMilli();
+ long expiration = presignedUrlRequestArgumentCaptor.getValue().getExpiration().toInstant().toEpochMilli();
+ Assertions.assertTrue(expiration - now > 299_000);
+ Assertions.assertTrue(expiration - now < 301_000);
+ Assertions.assertEquals(csvUploadConfig.getBucketName(), presignedUrlRequestArgumentCaptor.getValue().getBucketName());
+ Assertions.assertEquals(PARTNER_ID_1 + ".zip", presignedUrlRequestArgumentCaptor.getValue().getKey());
+ Assertions.assertEquals(HttpMethod.GET, presignedUrlRequestArgumentCaptor.getValue().getMethod());
}
private void checkCsv(byte[] csvBytes, int expectedCols, int expectedRows) throws IOException, CsvException {
String csv = new String(csvBytes, StandardCharsets.UTF_8);
CSVParser csvParser = new CSVParserBuilder()
- .withSeparator('\t')
- .build();
+ .withSeparator('\t')
+ .build();
try (CSVReader csvReader = new CSVReaderBuilder(new StringReader(csv))
- .withCSVParser(csvParser)
- .build()
+ .withCSVParser(csvParser)
+ .build()
) {
List csvEntries = csvReader.readAll();
Assertions.assertEquals(expectedRows, csvEntries.size());
diff --git a/src/test/java/app/coronawarn/quicktest/service/CancellationCsvTest.java b/src/test/java/app/coronawarn/quicktest/service/CancellationCsvTest.java
index 8031c3ca..9de4e186 100644
--- a/src/test/java/app/coronawarn/quicktest/service/CancellationCsvTest.java
+++ b/src/test/java/app/coronawarn/quicktest/service/CancellationCsvTest.java
@@ -92,7 +92,7 @@ class CancellationCsvTest {
@BeforeEach
void setUp() {
shortTermArchiveRepository.deleteAll();
- longTermArchiveRepository.deleteAllByTenantId(PARTNER_ID);
+ longTermArchiveRepository.deleteAllByTenantId(PARTNER_ID_HASH);
cancellationRepository.deleteAll();
}