From fe0763cf84a00712a486a6e13e3905836e06afb9 Mon Sep 17 00:00:00 2001 From: Felix Dittrich <31076102+f11h@users.noreply.github.com> Date: Mon, 23 Jan 2023 13:56:10 +0100 Subject: [PATCH] Feat: Endpoint to export Archive (#264) * Work in progress * Add Unit Test for CSV Export Add SHA256, Entity-Count and File-Size of CSV to cancellation Entity * Add Endpoint to download Archive for Partner Small Refactoring * Update Role Name Add required profile to activate Archive Export Controller * Update Role Name Add required profile to activate Archive Export Controller * Add Log Message for Archive Export * Checkstyle * Fix Header * Fix Header --- .../archive/repository/ArchiveRepository.java | 28 ++- .../quicktest/config/SecurityConfig.java | 1 + .../controller/ArchiveExportController.java | 104 ++++++++++ .../quicktest/service/ArchiveService.java | 75 ++++++- .../CancellationSchedulingService.java | 60 +----- .../ArchiveExportControllerTest.java | 194 ++++++++++++++++++ 6 files changed, 400 insertions(+), 62 deletions(-) create mode 100644 src/main/java/app/coronawarn/quicktest/controller/ArchiveExportController.java create mode 100644 src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java diff --git a/src/main/java/app/coronawarn/quicktest/archive/repository/ArchiveRepository.java b/src/main/java/app/coronawarn/quicktest/archive/repository/ArchiveRepository.java index 3a4939ef..3d21f2d1 100644 --- a/src/main/java/app/coronawarn/quicktest/archive/repository/ArchiveRepository.java +++ b/src/main/java/app/coronawarn/quicktest/archive/repository/ArchiveRepository.java @@ -78,14 +78,32 @@ public List findAll() { * * @return {@link List} of {@link Archive} */ - public List findAllByPocId(final String pocId) { - this.em.getTransaction().begin(); + public List findAllByPocId(final String pocId, final String tenantId) { + em.getTransaction().begin(); final List result = em - .createQuery("SELECT a FROM Archive a WHERE a.pocId = ?1", Archive.class) - .setParameter(1, pocId).getResultList(); + .createQuery("SELECT a FROM Archive a WHERE a.pocId = ?1 AND a.tenantId = ?2", Archive.class) + .setParameter(1, pocId) + .setParameter(2, tenantId) + .getResultList(); - this.em.getTransaction().commit(); + em.getTransaction().commit(); + return result; + } + + /** + * Returns all entries by tenantId. + * + * @return {@link List} of {@link Archive} + */ + public List findAllByTenantId(final String tenantId) { + em.getTransaction().begin(); + + final List result = em + .createQuery("SELECT a FROM Archive a WHERE a.tenantId = ?1", Archive.class) + .setParameter(1, tenantId).getResultList(); + + em.getTransaction().commit(); return result; } diff --git a/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java b/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java index 01925004..b8aeae92 100644 --- a/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java +++ b/src/main/java/app/coronawarn/quicktest/config/SecurityConfig.java @@ -51,6 +51,7 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { public static final String ROLE_TENANT_COUNTER = "ROLE_c19_quick_tenant_test_counter"; 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"; 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 new file mode 100644 index 00000000..c7a2dcfc --- /dev/null +++ b/src/main/java/app/coronawarn/quicktest/controller/ArchiveExportController.java @@ -0,0 +1,104 @@ +/*- + * ---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.controller; + +import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_OPERATOR; + +import app.coronawarn.quicktest.service.ArchiveService; +import com.opencsv.exceptions.CsvDataTypeMismatchException; +import com.opencsv.exceptions.CsvRequiredFieldEmptyException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + + +@Slf4j +@RestController +@RequestMapping(value = "/api/archive") +@RequiredArgsConstructor +@Profile("archive_export") +public class ArchiveExportController { + + private final ArchiveService archiveService; + + /** + * Endpoint for downloading archived entities. + * + * @return CSV with all archived data. + */ + @Operation( + summary = "Download Archive CSV-File", + 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), + @Parameter( + in = ParameterIn.QUERY, + name = "pocId", + description = "Filter for entities with given pocId") + } + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successful") + }) + @GetMapping(value = "/{partnerId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @Secured({ROLE_ARCHIVE_OPERATOR}) + public ResponseEntity exportArchive(@PathVariable("partnerId") String partnerId, + Authentication authentication) { + + try { + ArchiveService.CsvExportFile csv = archiveService.createCsv(partnerId); + + log.info("Archive Export triggered for PartnerId: {} by {}, FileSize: {}", + partnerId, authentication.getName(), csv.getCsvBytes().length); + + return ResponseEntity + .status(HttpStatus.OK) + .contentLength(csv.getCsvBytes().length) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=quicktest_export.csv") + .body(csv.getCsvBytes()); + + } catch (CsvRequiredFieldEmptyException | CsvDataTypeMismatchException e) { + log.error("Failed to create CSV: {}", e.getMessage()); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create CSV."); + } + } +} diff --git a/src/main/java/app/coronawarn/quicktest/service/ArchiveService.java b/src/main/java/app/coronawarn/quicktest/service/ArchiveService.java index 0c012490..8fc0c033 100644 --- a/src/main/java/app/coronawarn/quicktest/service/ArchiveService.java +++ b/src/main/java/app/coronawarn/quicktest/service/ArchiveService.java @@ -31,6 +31,12 @@ import app.coronawarn.quicktest.service.cryption.CryptionService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.opencsv.CSVWriter; +import com.opencsv.bean.StatefulBeanToCsv; +import com.opencsv.bean.StatefulBeanToCsvBuilder; +import com.opencsv.exceptions.CsvDataTypeMismatchException; +import com.opencsv.exceptions.CsvRequiredFieldEmptyException; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -42,6 +48,7 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.javacrumbs.shedlock.core.LockExtender; @@ -168,13 +175,18 @@ public void moveToArchiveByTenantId(String tenantId) { } /** - * Get longterm archives by pocId. + * Get decrypted entities from longterm archive. + * + * @param pocId (optional) pocId to filter for + * @param tenantId tenantID to filter for */ - public List getQuicktestsFromLongterm(final String pocId, final String tenantId) - throws JsonProcessingException { - List allByPocId = longTermArchiveRepository.findAllByPocId(createHash(pocId)); - List dtos = new ArrayList<>(allByPocId.size()); - for (Archive archive : allByPocId) { + public List getQuicktestsFromLongterm(final String pocId, final String tenantId) { + List entities = pocId != null + ? longTermArchiveRepository.findAllByPocId(createHash(pocId), createHash(tenantId)) + : longTermArchiveRepository.findAllByTenantId(createHash(tenantId)); + + List dtos = new ArrayList<>(entities.size()); + for (Archive archive : entities) { try { final String decrypt = keyProvider.decrypt(archive.getSecret(), tenantId); final String json = cryptionService.getAesCryption().decrypt(decrypt, archive.getCiphertext()); @@ -188,6 +200,57 @@ public List getQuicktestsFromLongterm(final String pocId, fi return dtos; } + @RequiredArgsConstructor + @Getter + public static class CsvExportFile { + private final byte[] csvBytes; + private final int totalEntityCount; + } + + /** + * Create a CSV containing given Quicktest-Archive-Entities. + * + * @param partnerId Partner for which the CSV should be created. + * @return byte-array representing a CSV. + */ + public CsvExportFile createCsv(String partnerId) + throws CsvRequiredFieldEmptyException, CsvDataTypeMismatchException { + + StringWriter stringWriter = new StringWriter(); + CSVWriter csvWriter = new CSVWriter( + stringWriter, + '\t', + CSVWriter.DEFAULT_QUOTE_CHARACTER, + + '\\', + CSVWriter.DEFAULT_LINE_END); + + StatefulBeanToCsv beanToCsv = + new StatefulBeanToCsvBuilder(csvWriter).build(); + + int page = 0; + int pageSize = 500; + int totalEntityCount = 0; + List quicktests; + do { + log.info("Loading Archive Chunk {} for Partner {}", page, partnerId); + quicktests = getQuicktestsFromLongtermByTenantId(partnerId, page, pageSize); + totalEntityCount += quicktests.size(); + log.info("Found {} Quicktests in Archive for Chunk {} for Partner {}", quicktests.size(), page, partnerId); + beanToCsv.write(quicktests); + page++; + + try { + LockExtender.extendActiveLock(Duration.ofMinutes(10), Duration.ZERO); + } catch (LockExtender.NoActiveLockException ignored) { + // Exception will be thrown if Job is executed outside Scheduler Context + } + } while (!quicktests.isEmpty()); + log.info("Got {} Quicktests for Partner {}", totalEntityCount, partnerId); + + return new CsvExportFile(stringWriter.toString().getBytes(StandardCharsets.UTF_8), totalEntityCount); + } + /** * Get longterm archives by tenantId. */ diff --git a/src/main/java/app/coronawarn/quicktest/service/CancellationSchedulingService.java b/src/main/java/app/coronawarn/quicktest/service/CancellationSchedulingService.java index cdab0616..375c661b 100644 --- a/src/main/java/app/coronawarn/quicktest/service/CancellationSchedulingService.java +++ b/src/main/java/app/coronawarn/quicktest/service/CancellationSchedulingService.java @@ -20,25 +20,17 @@ package app.coronawarn.quicktest.service; -import app.coronawarn.quicktest.archive.domain.ArchiveCipherDtoV1; import app.coronawarn.quicktest.config.CsvUploadConfig; import app.coronawarn.quicktest.domain.Cancellation; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; -import com.opencsv.CSVWriter; -import com.opencsv.bean.StatefulBeanToCsv; -import com.opencsv.bean.StatefulBeanToCsvBuilder; import java.io.ByteArrayInputStream; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.time.Duration; import java.time.ZonedDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import net.javacrumbs.shedlock.core.LockExtender; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.keycloak.representations.idm.GroupRepresentation; import org.springframework.scheduling.annotation.Scheduled; @@ -102,63 +94,29 @@ private void processCsvUploadBatchRecursion(List cancellations) { for (Cancellation cancellation : cancellations) { log.info("Processing CSV for Partner {}", cancellation.getPartnerId()); try { - StringWriter stringWriter = new StringWriter(); - CSVWriter csvWriter = new CSVWriter( - stringWriter, - '\t', - CSVWriter.DEFAULT_QUOTE_CHARACTER, - - '\\', - CSVWriter.DEFAULT_LINE_END); - - StatefulBeanToCsv beanToCsv = - new StatefulBeanToCsvBuilder(csvWriter).build(); - - int page = 0; - int pageSize = 500; - int totalEntityCount = 0; - List quicktests; - do { - log.info("Loading Archive Chunk {} for Partner {}", page, cancellation.getPartnerId()); - quicktests = archiveService.getQuicktestsFromLongtermByTenantId( - cancellation.getPartnerId(), page, pageSize); - totalEntityCount += quicktests.size(); - log.info("Found {} Quicktests in Archive for Chunk {} for Partner {}", - quicktests.size(), page, cancellation.getPartnerId()); - beanToCsv.write(quicktests); - page++; - - try { - LockExtender.extendActiveLock(Duration.ofMinutes(10), Duration.ZERO); - } catch (LockExtender.NoActiveLockException ignored) { - // Exception will be thrown if Job is executed outside Sheduler Context - } - } while (!quicktests.isEmpty()); - log.info("Got {} Quicktests for Partner {}", totalEntityCount, cancellation.getPartnerId()); - - byte[] csvBytes = stringWriter.toString().getBytes(StandardCharsets.UTF_8); + ArchiveService.CsvExportFile csv = archiveService.createCsv(cancellation.getPartnerId()); String objectId = cancellation.getPartnerId() + ".csv"; ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(csvBytes.length); + metadata.setContentLength(csv.getCsvBytes().length); s3Client.putObject( s3Config.getBucketName(), objectId, - new ByteArrayInputStream(csvBytes), metadata); + new ByteArrayInputStream(csv.getCsvBytes()), metadata); log.info("File stored to S3 with id: {}, size: {}, hash: {}", - objectId, csvBytes.length, getHash(csvBytes)); + objectId, csv.getCsvBytes().length, getHash(csv.getCsvBytes())); - if (cancellation.getDbEntityCount() == totalEntityCount) { + if (cancellation.getDbEntityCount() == csv.getTotalEntityCount()) { cancellationService.updateCsvCreated(cancellation, ZonedDateTime.now(), objectId, - getHash(csvBytes), totalEntityCount, csvBytes.length); + getHash(csv.getCsvBytes()), csv.getTotalEntityCount(), csv.getCsvBytes().length); } else { log.error("Difference between actual and expected EntityCount in CSV File for partner {}. " - + "Expected: {}, Acutal: {}, CSV Export will not be marked as finished.", - cancellation.getPartnerId(), cancellation.getDbEntityCount(), totalEntityCount); + + "Expected: {}, Actual: {}, CSV Export will not be marked as finished.", + cancellation.getPartnerId(), cancellation.getDbEntityCount(), csv.getTotalEntityCount()); cancellationService.updateDataExportError(cancellation, "CSV Export Delta detected. " - + "Expected: " + cancellation.getDbEntityCount() + " Actual: " + totalEntityCount); + + "Expected: " + cancellation.getDbEntityCount() + " Actual: " + csv.getTotalEntityCount()); } } catch (Exception e) { String errorMessage = e.getClass().getName() + ": " + e.getMessage(); diff --git a/src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java b/src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java new file mode 100644 index 00000000..baf3b4ca --- /dev/null +++ b/src/test/java/app/coronawarn/quicktest/controller/ArchiveExportControllerTest.java @@ -0,0 +1,194 @@ +/*- + * ---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.controller; + +import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_ARCHIVE_OPERATOR; +import static app.coronawarn.quicktest.config.SecurityConfig.ROLE_COUNTER; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import app.coronawarn.quicktest.config.QuicktestKeycloakSpringBootConfigResolver; +import app.coronawarn.quicktest.domain.QuickTestArchive; +import app.coronawarn.quicktest.model.Sex; +import app.coronawarn.quicktest.repository.QuickTestArchiveRepository; +import app.coronawarn.quicktest.service.ArchiveSchedulingService; +import app.coronawarn.quicktest.utils.Utilities; +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.opencsv.CSVParser; +import com.opencsv.CSVParserBuilder; +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.exceptions.CsvException; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = "keycloak-admin.realm=REALM") +@ActiveProfiles({"test", "archive_export"}) +@AutoConfigureMockMvc +@Import({UserManagementControllerUtils.class, Utilities.class}) +@ComponentScan(basePackageClasses = {KeycloakSecurityComponents.class, QuicktestKeycloakSpringBootConfigResolver.class}) +class ArchiveExportControllerTest extends ServletKeycloakAuthUnitTestingSupport { + + @Autowired + private QuickTestArchiveRepository shortTermArchiveRepository; + + @Autowired + private ArchiveSchedulingService archiveSchedulingService; + + private final static String userId = "user-id"; + + public static final String PARTNER_ID_1 = "P10000"; + + public static final String PARTNER_ID_2 = "P10001"; + + public static final String POC_ID_1 = "Poc_42"; + + public static final String POC_ID_2 = "Poc_43"; + + @Test + @WithMockKeycloakAuth( + authorities = ROLE_ARCHIVE_OPERATOR, + claims = @OpenIdClaims(sub = userId) + ) + void downloadArchiveByPartnerId() throws Exception { + + shortTermArchiveRepository.save(buildQuickTestArchive(PARTNER_ID_1, POC_ID_1)); + shortTermArchiveRepository.save(buildQuickTestArchive(PARTNER_ID_1, POC_ID_1)); + shortTermArchiveRepository.save(buildQuickTestArchive(PARTNER_ID_1, POC_ID_1)); + shortTermArchiveRepository.save(buildQuickTestArchive(PARTNER_ID_1, POC_ID_2)); + + shortTermArchiveRepository.save(buildQuickTestArchive(PARTNER_ID_2, POC_ID_1)); + shortTermArchiveRepository.save(buildQuickTestArchive(PARTNER_ID_2, POC_ID_1)); + shortTermArchiveRepository.save(buildQuickTestArchive(PARTNER_ID_2, POC_ID_2)); + + archiveSchedulingService.moveToArchiveJob(); + + MvcResult mvcResult = mockMvc().perform(MockMvcRequestBuilders + .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(), 27, 5); + + mvcResult = mockMvc().perform(MockMvcRequestBuilders + .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(), 27, 4); + + mvcResult = mockMvc().perform(MockMvcRequestBuilders + .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)); + checkCsv(mvcResult.getResponse().getContentAsByteArray(), 0, 0); + + } + + @Test + @WithMockKeycloakAuth( + authorities = ROLE_COUNTER, + claims = @OpenIdClaims(sub = userId) + ) + void downloadArchiveWrongRole() throws Exception { + + mockMvc().perform(MockMvcRequestBuilders + .get("/api/archive/" + PARTNER_ID_1)) + .andExpect(status().isForbidden()); + } + + 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(); + + try (CSVReader csvReader = new CSVReaderBuilder(new StringReader(csv)) + .withCSVParser(csvParser) + .build() + ) { + List csvEntries = csvReader.readAll(); + Assertions.assertEquals(expectedRows, csvEntries.size()); + if (expectedRows > 0) { + Assertions.assertEquals(expectedCols, csvEntries.get(0).length); + } + } + } + + private QuickTestArchive buildQuickTestArchive(String tenantId, String pocId) { + QuickTestArchive qta = new QuickTestArchive(); + qta.setShortHashedGuid(HexUtils.toHexString(RandomUtils.nextBytes(4))); + qta.setHashedGuid(HexUtils.toHexString(RandomUtils.nextBytes(32))); + qta.setTenantId(tenantId); + qta.setPocId(pocId); + qta.setCreatedAt(LocalDateTime.now().minusMonths(3)); + qta.setUpdatedAt(LocalDateTime.now().minusMonths(2)); + qta.setConfirmationCwa(Boolean.TRUE); + qta.setTestResult(Short.valueOf("6")); + qta.setPrivacyAgreement(Boolean.TRUE); + qta.setLastName("last_name"); + qta.setFirstName("first_name"); + qta.setEmail("email"); + qta.setPhoneNumber("phone_number"); + qta.setSex(Sex.MALE); + qta.setStreet("street"); + qta.setHouseNumber("house_number"); + qta.setZipCode("zip_code"); + qta.setCity("city"); + qta.setTestBrandId("test_brand_id"); + qta.setTestBrandName("test_brand_name, Ltd, another_part_of_test_brand_name"); + qta.setBirthday("2000-01-01"); + qta.setPdf("PDF".getBytes()); + qta.setTestResultServerHash("test_result_server_hash"); + qta.setDcc("dcc"); + qta.setAdditionalInfo("additional_info"); + qta.setGroupName("group_name"); + return qta; + } +}