diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java b/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java index ef74d5ec..7dc115fc 100644 --- a/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java +++ b/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java @@ -1,6 +1,7 @@ package ch.admin.bag.covidcertificate.api; import ch.admin.bag.covidcertificate.api.exception.AuthorizationError; +import ch.admin.bag.covidcertificate.api.exception.BiDataError; import ch.admin.bag.covidcertificate.api.exception.ConvertCertificateError; import ch.admin.bag.covidcertificate.api.exception.CreateCertificateError; import ch.admin.bag.covidcertificate.api.exception.CsvError; @@ -209,6 +210,9 @@ public class Constants { public static final CreateCertificateError DATE_OF_BIRTH_AFTER_CERTIFICATE_DATE = new CreateCertificateError(1004, "Invalid dateOfBirth! Must be before the certificate date", HttpStatus.BAD_REQUEST); + public static final BiDataError DATES_NOT_VALID = new BiDataError(1101, "The given dates sent to request BI data are not valid. Please define a week or a month e.g. from 2022-10-10 to 2022-10-16.", HttpStatus.BAD_REQUEST); + public static final BiDataError WRITING_CSV_RESULT_FAILED = new BiDataError(1102, "Writing the CSV result of the BI data export failed.", HttpStatus.INTERNAL_SERVER_ERROR); + public static final String VACCINATION_TOURIST_PRODUCT_CODE_SUFFIX = "_T"; public static final Integer EXPIRATION_PERIOD_24_MONTHS = 24; diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/exception/BiDataError.java b/src/main/java/ch/admin/bag/covidcertificate/api/exception/BiDataError.java new file mode 100644 index 00000000..9ff043d8 --- /dev/null +++ b/src/main/java/ch/admin/bag/covidcertificate/api/exception/BiDataError.java @@ -0,0 +1,35 @@ +package ch.admin.bag.covidcertificate.api.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.util.Objects; + +@AllArgsConstructor +@Getter +public class BiDataError { + private final int errorCode; + private final String errorMessage; + private final HttpStatus httpStatus; + + @Override + public String toString() { + return "{\"errorCode\":" + errorCode + "," + + "\"errorMessage\":\"" + errorMessage + "\"," + + "\"httpStatus\":\"" + httpStatus.name() + "\"}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BiDataError that = (BiDataError) o; + return errorCode == that.errorCode && errorMessage.equals(that.errorMessage) && httpStatus == that.httpStatus; + } + + @Override + public int hashCode() { + return Objects.hash(errorCode, errorMessage, httpStatus); + } +} diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/exception/BiDataException.java b/src/main/java/ch/admin/bag/covidcertificate/api/exception/BiDataException.java new file mode 100644 index 00000000..c2d71cea --- /dev/null +++ b/src/main/java/ch/admin/bag/covidcertificate/api/exception/BiDataException.java @@ -0,0 +1,15 @@ +package ch.admin.bag.covidcertificate.api.exception; + +import lombok.Getter; +import org.springframework.core.NestedRuntimeException; + +@Getter +public class BiDataException extends NestedRuntimeException { + + private final BiDataError biDataError; + + public BiDataException(BiDataError error) { + super(error.getErrorMessage()); + this.biDataError = error; + } +} diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/response/BiDataDto.java b/src/main/java/ch/admin/bag/covidcertificate/api/response/BiDataDto.java new file mode 100644 index 00000000..5934bc1f --- /dev/null +++ b/src/main/java/ch/admin/bag/covidcertificate/api/response/BiDataDto.java @@ -0,0 +1,28 @@ +package ch.admin.bag.covidcertificate.api.response; + +import ch.admin.bag.covidcertificate.domain.BiData; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class BiDataDto { + UUID id; + LocalDateTime timestamp; + String type; + String value; + String details; + String country; + String systemSource; + String apiGatewayId; + String inAppDeliveryCode; + Boolean fraud; + String keyIdentifier; +} diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/response/BiDataResponseDto.java b/src/main/java/ch/admin/bag/covidcertificate/api/response/BiDataResponseDto.java new file mode 100644 index 00000000..b4cb2a91 --- /dev/null +++ b/src/main/java/ch/admin/bag/covidcertificate/api/response/BiDataResponseDto.java @@ -0,0 +1,20 @@ +package ch.admin.bag.covidcertificate.api.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class BiDataResponseDto { + + @NonNull + private byte[] zip; +} diff --git a/src/main/java/ch/admin/bag/covidcertificate/authorization/AuthorizationService.java b/src/main/java/ch/admin/bag/covidcertificate/authorization/AuthorizationService.java index e454519d..6bd9b6d0 100644 --- a/src/main/java/ch/admin/bag/covidcertificate/authorization/AuthorizationService.java +++ b/src/main/java/ch/admin/bag/covidcertificate/authorization/AuthorizationService.java @@ -98,22 +98,14 @@ public List getRoleMapping() { } /** - * Returns true for given function IF: - * - *
  • - * The given function is only permitted when both conditions are valid. + * Returns true for given function if the one-of setting contains the role needed + * for the function to be accessed. If one-of isn't configured false will be returned. * * @param roles the user's roles * @param function the function to check - * @return true only if both mandatory and one-of are valid + * @return true for given function if the one-of setting contains the role needed + * for the function to be accessed. If one-of isn't configured false will be returned. */ - - public boolean isGranted(Set roles, ServiceData.Function function) { boolean isActive = function.isBetween(LocalDateTime.now()); if (!isActive) { diff --git a/src/main/java/ch/admin/bag/covidcertificate/domain/BiData.java b/src/main/java/ch/admin/bag/covidcertificate/domain/BiData.java new file mode 100644 index 00000000..16e98190 --- /dev/null +++ b/src/main/java/ch/admin/bag/covidcertificate/domain/BiData.java @@ -0,0 +1,18 @@ +package ch.admin.bag.covidcertificate.domain; + +import java.time.LocalDateTime; +import java.util.UUID; + +public interface BiData { + UUID getId(); + LocalDateTime getTimestamp(); + String getType(); + String getValue(); + String getDetails(); + String getCountry(); + String getSystemSource(); + String getApiGatewayId(); + String getInAppDeliveryCode(); + Boolean getFraud(); + String getKeyIdentifier(); +} diff --git a/src/main/java/ch/admin/bag/covidcertificate/domain/KpiDataRepository.java b/src/main/java/ch/admin/bag/covidcertificate/domain/KpiDataRepository.java index a2e1c002..bce7ca0a 100644 --- a/src/main/java/ch/admin/bag/covidcertificate/domain/KpiDataRepository.java +++ b/src/main/java/ch/admin/bag/covidcertificate/domain/KpiDataRepository.java @@ -1,11 +1,31 @@ package ch.admin.bag.covidcertificate.domain; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; @Repository public interface KpiDataRepository extends JpaRepository { KpiData findByUvci(String uvci); + + @Query(value = "select k.id as id" + + ", k.timestamp as timestamp" + + ", k.value as value" + + ", k.details as details" + + ", k.country as country" + + ", k.systemSource as systemSource" + + ", k.apiGatewayId as apiGatewayId" + + ", k.inAppDeliveryCode as inAppDeliveryCode" + + ", r.fraud as fraud" + + ", k.keyIdentifier as keyIdentifier " + + "from KpiData k " + + "LEFT JOIN Revocation r on r.uvci = k.uvci " + + "WHERE k.timestamp BETWEEN :fromDateTime AND :toDateTime " + + "ORDER BY k.timestamp asc") + List findAllByDateRange(@Param("fromDateTime") LocalDateTime fromDateTime, @Param("toDateTime") LocalDateTime toDateTime); } diff --git a/src/main/java/ch/admin/bag/covidcertificate/service/BiDataService.java b/src/main/java/ch/admin/bag/covidcertificate/service/BiDataService.java new file mode 100644 index 00000000..275f566e --- /dev/null +++ b/src/main/java/ch/admin/bag/covidcertificate/service/BiDataService.java @@ -0,0 +1,116 @@ +package ch.admin.bag.covidcertificate.service; + +import ch.admin.bag.covidcertificate.api.exception.BiDataException; +import ch.admin.bag.covidcertificate.api.response.BiDataDto; +import ch.admin.bag.covidcertificate.api.response.BiDataResponseDto; +import ch.admin.bag.covidcertificate.domain.BiData; +import ch.admin.bag.covidcertificate.domain.KpiDataRepository; +import com.opencsv.CSVWriter; +import com.opencsv.bean.StatefulBeanToCsv; +import com.opencsv.bean.StatefulBeanToCsvBuilder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static ch.admin.bag.covidcertificate.api.Constants.DATES_NOT_VALID; +import static ch.admin.bag.covidcertificate.api.Constants.WRITING_CSV_RESULT_FAILED; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BiDataService { + + private final KpiDataRepository kpiDataRepository; + + public BiDataResponseDto loadBiData(LocalDate fromDate, LocalDate toDate) throws BiDataException { + + validateDateRange(fromDate, toDate); + + LocalDateTime fromDateTime = fromDate.atTime(LocalTime.MIN); + LocalDateTime toDateTime = toDate.atTime(LocalTime.MAX); + List biDataList = this.kpiDataRepository.findAllByDateRange(fromDateTime, toDateTime); + List biDataDtoList = biDataList.stream().map(this::convert).collect(Collectors.toList()); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos)) { + byte[] csv = createCsvResponse(biDataDtoList); + var entry = new ZipEntry("kpi_prod_export_" + + fromDateTime.format(DateTimeFormatter.ISO_DATE) + + "-" + + toDateTime.format(DateTimeFormatter.ISO_DATE) + + ".csv"); + entry.setSize(csv.length); + zos.putNextEntry(entry); + zos.write(csv); + zos.closeEntry(); + zos.close(); + return new BiDataResponseDto(baos.toByteArray()); + } catch (IOException ex) { + log.error("IOException creating CSV response", ex); + throw new BiDataException(WRITING_CSV_RESULT_FAILED); + } + } + + private void validateDateRange(LocalDate fromDate, LocalDate toDate) { + if (fromDate == null || toDate == null) { + throw new BiDataException(DATES_NOT_VALID); + } + if (!fromDate.isBefore(toDate)) { + throw new BiDataException(DATES_NOT_VALID); + } + if (!(fromDate.plusMonths(1l).minusDays(1).isEqual(toDate) || fromDate.plusDays(6l).isEqual(toDate))) { + // time range is not one month or not one week + throw new BiDataException(DATES_NOT_VALID); + } + } + + private byte[] createCsvResponse(List biDataDtos) throws IOException { + var returnFile = writeCsv(biDataDtos, Charset.defaultCharset()); + return Files.readAllBytes(returnFile.toPath()); + } + + private File writeCsv(List biDataDtos, Charset charset) throws IOException { + var randomUUID = UUID.randomUUID(); + var file = File.createTempFile("bi_data_" + randomUUID, ".csv"); + try (var csvWriter = new CSVWriter(new FileWriter(file, charset))) { + StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(csvWriter) + .withSeparator(CSVWriter.DEFAULT_SEPARATOR) + .withApplyQuotesToAll(true) + .build(); + beanToCsv.write(biDataDtos); + return file; + } catch (Exception e) { + Files.delete(file.toPath()); + throw new BiDataException(WRITING_CSV_RESULT_FAILED); + } + } + + private BiDataDto convert(BiData biData) { + return new BiDataDto(biData.getId(), + biData.getTimestamp(), + biData.getType(), + biData.getValue(), + biData.getDetails(), + biData.getCountry(), + biData.getSystemSource(), + biData.getApiGatewayId(), + biData.getInAppDeliveryCode(), + biData.getFraud(), + biData.getKeyIdentifier()); + } +} diff --git a/src/main/java/ch/admin/bag/covidcertificate/web/controller/BiDataController.java b/src/main/java/ch/admin/bag/covidcertificate/web/controller/BiDataController.java new file mode 100644 index 00000000..cee9f8df --- /dev/null +++ b/src/main/java/ch/admin/bag/covidcertificate/web/controller/BiDataController.java @@ -0,0 +1,30 @@ +package ch.admin.bag.covidcertificate.web.controller; + +import ch.admin.bag.covidcertificate.api.response.BiDataResponseDto; +import ch.admin.bag.covidcertificate.service.BiDataService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +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 java.time.LocalDate; + +@RestController +@RequestMapping("/api/v1/bi-data/{fromDate}/{toDate}") +@RequiredArgsConstructor +@Slf4j +public class BiDataController { + + private final BiDataService biDataService; + + @GetMapping() + public BiDataResponseDto loadBiData( + @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fromDate, + @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate toDate) { + + return biDataService.loadBiData(fromDate, toDate); + } +} diff --git a/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java b/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java index 28dca3fd..65cc7185 100644 --- a/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java +++ b/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java @@ -1,6 +1,7 @@ package ch.admin.bag.covidcertificate.web.controller; import ch.admin.bag.covidcertificate.api.exception.AuthorizationException; +import ch.admin.bag.covidcertificate.api.exception.BiDataException; import ch.admin.bag.covidcertificate.api.exception.CacheNotFoundException; import ch.admin.bag.covidcertificate.api.exception.ConvertCertificateException; import ch.admin.bag.covidcertificate.api.exception.CreateCertificateException; @@ -63,6 +64,11 @@ protected ResponseEntity handleRevocationException(RevocationException e return new ResponseEntity<>(ex.getError(), ex.getError().getHttpStatus()); } + @ExceptionHandler(value = {BiDataException.class}) + protected ResponseEntity handleBiDataException(BiDataException ex) { + return new ResponseEntity<>(ex.getBiDataError(), ex.getBiDataError().getHttpStatus()); + } + @ExceptionHandler(value = {AccessDeniedException.class, SecurityException.class}) protected ResponseEntity handleAccessDeniedException() { return new ResponseEntity<>(HttpStatus.FORBIDDEN); diff --git a/src/main/resources/application-authorization.yml b/src/main/resources/application-authorization.yml index 2943fa64..c29b669e 100644 --- a/src/main/resources/application-authorization.yml +++ b/src/main/resources/application-authorization.yml @@ -54,10 +54,12 @@ roles: dr-armee: "DR_Armee" dr-bv-intern: "DR_BV-Intern" dr-ggg: "DR_GGG" + bi: + data-access: "BI_DATA_ACCESS" groups: any: - ${roles.id.legacy.super-user},${roles.id.legacy.creator},${roles.id.user.api},${roles.id.user.web},${roles.id.user.revocator},${roles.id.cert.vacc},${roles.id.cert.tourist},${roles.id.cert.test},${roles.id.cert.antibody},${roles.id.cert.recovery},${roles.id.cert.rat},${roles.id.cert.exception},${roles.id.report.detail},${roles.id.report.agg},${roles.id.report.stats},${roles.id.cert.bulk},${roles.id.app.manager}} + ${roles.id.legacy.super-user},${roles.id.legacy.creator},${roles.id.user.api},${roles.id.user.web},${roles.id.user.revocator},${roles.id.cert.vacc},${roles.id.cert.tourist},${roles.id.cert.test},${roles.id.cert.antibody},${roles.id.cert.recovery},${roles.id.cert.rat},${roles.id.cert.exception},${roles.id.report.detail},${roles.id.report.agg},${roles.id.report.stats},${roles.id.cert.bulk},${roles.id.app.manager},${roles.id.bi.data-access}} legacy: ${roles.id.legacy.super-user},${roles.id.legacy.creator} users: @@ -207,6 +209,9 @@ roles: - intern: ${roles.id.dataroom.dr-zh} eiam: "9500.GGG-Covidcertificate.DR_ZH" claim: "bag-cc-dr_zh" + - intern: ${roles.id.bi.data-access} + eiam: "9500.GGG-Covidcertificate.BI_DATA_ACCESS" + claim: "bag-cc-bi-data-access" # 1st level: SERVICE # 2nd level: FUNCTION @@ -786,9 +791,15 @@ services: identifier: "clear-caches" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.legacy.super-user} + one-of: ${roles.id.app.manager} uri: "/api/v1/caches/clear" + bi-data-access: + identifier: "bi-data-access" + from: 2022-01-01T00:00:00 + until: 2099-12-31T23:59:59 + one-of: ${roles.id.bi.data-access} + uri: "/api/v1/bi-data/{fromDate}/{toDate}" ## R E P O R T ############################################################################## @@ -830,77 +841,77 @@ services: identifier: "report-a2" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.detail} + one-of: ${roles.id.report.detail} uri: "/api/v2/report/fraud/a2/by_uvci" report-a3: identifier: "report-a3" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.agg} + one-of: ${roles.id.report.agg} uri: "/api/v2/report/fraud/a3/for_timerange_by_users" report-a4: identifier: "report-a4" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.detail} + one-of: ${roles.id.report.detail} uri: "/api/v2/report/fraud/a4/by_users_and_types" report-a5: identifier: "report-a5" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.agg} + one-of: ${roles.id.report.agg} uri: "/api/v2/report/fraud/a5/(Aggregated Rep, URL TBD)" #TODO report-a6: identifier: "report-a6" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.detail} + one-of: ${roles.id.report.detail} uri: "/api/v2/report/fraud/a6/(Detail Rep, URL TBD)" #TODO report-a7: identifier: "report-a7" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.agg} + one-of: ${roles.id.report.agg} uri: "/api/v2/report/fraud/a7" report-a8: identifier: "report-a8" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.stats} + one-of: ${roles.id.report.stats} uri: "/api/v2/report/certificate/statistics/a8/for_timerange_by_week" report-a9: identifier: "report-a9" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.stats} + one-of: ${roles.id.report.stats} uri: "/api/v2/report/certificate/statistics/a9/for_timerange_by_types" report-a10: identifier: "report-a10" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.agg} + one-of: ${roles.id.report.agg} uri: "/api/v2/report/fraud/a10/for_timerange_by_types" report-a11: identifier: "report-a11" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.agg} + one-of: ${roles.id.report.agg} uri: "/api/v2/report/fraud/a11/for_timerange_by_canton" report-a12: identifier: "report-a12" from: 2022-01-01T00:00:00 until: 2099-12-31T23:59:59 - mandatory: ${roles.id.report.agg} + one-of: ${roles.id.report.agg} uri: "/api/v2/report/fraud/a12/for_transfer_codes" ## N O T I F I C A T I O N S ############################################################################## @@ -922,3 +933,4 @@ services: one-of: ${roles.id.app.manager} uri: "/api/v1/notifications" http: POST, DELETE + diff --git a/src/test/java/ch/admin/bag/covidcertificate/service/BiDataServiceTest.java b/src/test/java/ch/admin/bag/covidcertificate/service/BiDataServiceTest.java new file mode 100644 index 00000000..286f341d --- /dev/null +++ b/src/test/java/ch/admin/bag/covidcertificate/service/BiDataServiceTest.java @@ -0,0 +1,207 @@ +package ch.admin.bag.covidcertificate.service; + +import ch.admin.bag.covidcertificate.api.exception.BiDataException; +import ch.admin.bag.covidcertificate.api.response.BiDataResponseDto; +import ch.admin.bag.covidcertificate.domain.BiData; +import ch.admin.bag.covidcertificate.domain.KpiDataRepository; +import com.flextrade.jfixture.JFixture; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@RunWith(MockitoJUnitRunner.class) +class BiDataServiceTest { + + private final JFixture fixture = new JFixture(); + + @InjectMocks + private BiDataService biDataService; + + @Mock + private KpiDataRepository kpiDataRepository; + + @Test + void loadBiData_with_a_week_as_fromDate_and_toDate_should_succeed() { + // with data + LocalDate from = LocalDate.of(2022, 10, 10); + LocalDate to = LocalDate.of(2022, 10, 16); + List searchResult = this.createResultList(from, to); + when(kpiDataRepository.findAllByDateRange(any(), any())) + .thenReturn(searchResult); + + // do test + BiDataResponseDto response = this.biDataService.loadBiData(from, to); + + // check result + assertThat(response).isNotNull(); + assertThat(response.getZip()).isNotEmpty(); + } + + @Test + void loadBiData_with_fromDate_null_should_fail() { + LocalDate from = null; + LocalDate to = LocalDate.of(2022, 10, 18); + BiDataException biDataException = assertThrows(BiDataException.class, () -> this.biDataService.loadBiData(from, to)); + assertThat(biDataException).isNotNull(); + assertThat(biDataException.getBiDataError().getErrorMessage()).isEqualTo( + "The given dates sent to request BI data are not valid. Please define a week or a month e.g. from 2022-10-10 to 2022-10-16."); + assertThat(biDataException.getBiDataError().getErrorCode()).isEqualTo(1101); + assertThat(biDataException.getBiDataError().getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void loadBiData_with_toDate_null_should_fail() { + LocalDate from = LocalDate.of(2022, 10, 10); + LocalDate to = null; + BiDataException biDataException = assertThrows(BiDataException.class, () -> this.biDataService.loadBiData(from, to)); + assertThat(biDataException).isNotNull(); + assertThat(biDataException.getBiDataError().getErrorMessage()).isEqualTo( + "The given dates sent to request BI data are not valid. Please define a week or a month e.g. from 2022-10-10 to 2022-10-16."); + assertThat(biDataException.getBiDataError().getErrorCode()).isEqualTo(1101); + assertThat(biDataException.getBiDataError().getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void loadBiData_with_more_than_a_week_as_fromDate_and_toDate_should_fail() { + LocalDate from = LocalDate.of(2022, 10, 10); + LocalDate to = LocalDate.of(2022, 10, 18); + BiDataException biDataException = assertThrows(BiDataException.class, () -> this.biDataService.loadBiData(from, to)); + assertThat(biDataException).isNotNull(); + assertThat(biDataException.getBiDataError().getErrorMessage()).isEqualTo( + "The given dates sent to request BI data are not valid. Please define a week or a month e.g. from 2022-10-10 to 2022-10-16."); + assertThat(biDataException.getBiDataError().getErrorCode()).isEqualTo(1101); + assertThat(biDataException.getBiDataError().getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void loadBiData_with_less_than_a_week_as_fromDate_and_toDate_should_fail() { + LocalDate from = LocalDate.of(2022, 10, 10); + LocalDate to = LocalDate.of(2022, 10, 14); + BiDataException biDataException = assertThrows(BiDataException.class, () -> this.biDataService.loadBiData(from, to)); + assertThat(biDataException).isNotNull(); + assertThat(biDataException.getBiDataError().getErrorMessage()).isEqualTo( + "The given dates sent to request BI data are not valid. Please define a week or a month e.g. from 2022-10-10 to 2022-10-16."); + assertThat(biDataException.getBiDataError().getErrorCode()).isEqualTo(1101); + assertThat(biDataException.getBiDataError().getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void loadBiData_with_month_November_as_fromDate_and_toDate_should_succeed() { + // with data + LocalDate from = LocalDate.of(2022, 11, 1); + LocalDate to = LocalDate.of(2022, 11, 30); + List searchResult = this.createResultList(from, to); + when(kpiDataRepository.findAllByDateRange(any(), any())) + .thenReturn(searchResult); + + // do test + BiDataResponseDto response = this.biDataService.loadBiData(from, to); + + // check result + assertThat(response).isNotNull(); + assertThat(response.getZip()).isNotEmpty(); + } + + @Test + void loadBiData_with_month_December_as_fromDate_and_toDate_should_succeed() { + // with data + LocalDate from = LocalDate.of(2022, 12, 1); + LocalDate to = LocalDate.of(2022, 12, 31); + List searchResult = this.createResultList(from, to); + when(kpiDataRepository.findAllByDateRange(any(), any())) + .thenReturn(searchResult); + + // do test + BiDataResponseDto response = this.biDataService.loadBiData(from, to); + + // check result + assertThat(response).isNotNull(); + assertThat(response.getZip()).isNotEmpty(); + } + + @Test + void loadBiData_with_month_February_as_fromDate_and_toDate_should_succeed() { + // with data + LocalDate from = LocalDate.of(2022, 2, 1); + LocalDate to = LocalDate.of(2022, 2, 28); + List searchResult = this.createResultList(from, to); + when(kpiDataRepository.findAllByDateRange(any(), any())) + .thenReturn(searchResult); + + // do test + BiDataResponseDto response = this.biDataService.loadBiData(from, to); + + // check result + assertThat(response).isNotNull(); + assertThat(response.getZip()).isNotEmpty(); + } + + @Test + void loadBiData_with_month_February_of_leap_year_as_fromDate_and_toDate_should_succeed() { + // with data + LocalDate from = LocalDate.of(2020, 2, 1); + LocalDate to = LocalDate.of(2020, 2, 29); + List searchResult = this.createResultList(from, to); + when(kpiDataRepository.findAllByDateRange(any(), any())) + .thenReturn(searchResult); + + // do test + BiDataResponseDto response = this.biDataService.loadBiData(from, to); + + // check result + assertThat(response).isNotNull(); + assertThat(response.getZip()).isNotEmpty(); + } + + private List createResultList(LocalDate fromDate, LocalDate toDate) { + List result = new ArrayList<>(); + long diff = ChronoUnit.DAYS.between(fromDate, toDate); + for(long index = 0; index <= diff; index ++) { + BiDataMock mock = fixture.create(BiDataMock.class); + mock.setTimestamp(fromDate.plusDays(index).atTime(LocalTime.NOON)); + result.add(mock); + } + return result; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public class BiDataMock implements BiData { + public UUID id; + public LocalDateTime timestamp; + public String type; + public String value; + public String details; + public String country; + public String systemSource; + public String apiGatewayId; + public String inAppDeliveryCode; + public Boolean fraud; + public String keyIdentifier; + } +} diff --git a/src/test/java/ch/admin/bag/covidcertificate/web/controller/BiDataControllerTest.java b/src/test/java/ch/admin/bag/covidcertificate/web/controller/BiDataControllerTest.java new file mode 100644 index 00000000..02d4cc3d --- /dev/null +++ b/src/test/java/ch/admin/bag/covidcertificate/web/controller/BiDataControllerTest.java @@ -0,0 +1,92 @@ +package ch.admin.bag.covidcertificate.web.controller; + +import ch.admin.bag.covidcertificate.api.exception.BiDataException; +import ch.admin.bag.covidcertificate.api.response.BiDataResponseDto; +import ch.admin.bag.covidcertificate.service.BiDataService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.flextrade.jfixture.JFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static ch.admin.bag.covidcertificate.api.Constants.DATES_NOT_VALID; +import static ch.admin.bag.covidcertificate.api.Constants.WRITING_CSV_RESULT_FAILED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; + +@ExtendWith(MockitoExtension.class) +class BiDataControllerTest { + private static final String B_URL = "/api/v1/bi-data/{fromDate}/{toDate}"; + private static final JFixture fixture = new JFixture(); + private final ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().modules(new JavaTimeModule()).build(); + @InjectMocks + private BiDataController controller; + @Mock + private BiDataService biDataService; + private MockMvc mockMvc; + + @BeforeEach + void setupMocks() { + this.mockMvc = standaloneSetup(controller, new ResponseStatusExceptionHandler()).build(); + } + + @Test + void loadBiData_with_valid_dates() throws Exception { + LocalDate from = LocalDate.of(2022, 10, 10); + LocalDate to = LocalDate.of(2022, 10, 16); + BiDataResponseDto biDataResponseDto = fixture.create(BiDataResponseDto.class); + when(biDataService.loadBiData(from, to)).thenReturn(biDataResponseDto); + + String url = B_URL.replace("{fromDate}", from.format(DateTimeFormatter.ISO_DATE)); + url = url.replace("{toDate}", to.format(DateTimeFormatter.ISO_DATE)); + + MvcResult result = mockMvc + .perform(get(url).header("Authorization", fixture.create(String.class))) + .andExpect(status().isOk()) + .andReturn(); + + BiDataResponseDto expectedDto = mapper.readValue(result.getResponse().getContentAsString(), BiDataResponseDto.class); + assertEquals(expectedDto, biDataResponseDto); + } + + @Test + void loadBiData_with_invalid_dates() throws Exception { + LocalDate from = LocalDate.of(2022, 10, 10); + LocalDate to = LocalDate.of(2022, 10, 14); + when(biDataService.loadBiData(from, to)).thenThrow(new BiDataException(DATES_NOT_VALID)); + + String url = B_URL.replace("{fromDate}", from.format(DateTimeFormatter.ISO_DATE)); + url = url.replace("{toDate}", to.format(DateTimeFormatter.ISO_DATE)); + + mockMvc.perform(get(url).header("Authorization", fixture.create(String.class))) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertEquals(DATES_NOT_VALID.toString(), result.getResponse().getContentAsString())); + } + + @Test + void loadBiData_with_valid_dates_but_BiDataException_with_WRITING_CSV_RESULT_FAILED_error() throws Exception { + LocalDate from = LocalDate.of(2022, 10, 10); + LocalDate to = LocalDate.of(2022, 10, 16); + when(biDataService.loadBiData(from, to)).thenThrow(new BiDataException(WRITING_CSV_RESULT_FAILED)); + + String url = B_URL.replace("{fromDate}", from.format(DateTimeFormatter.ISO_DATE)); + url = url.replace("{toDate}", to.format(DateTimeFormatter.ISO_DATE)); + + mockMvc.perform(get(url).header("Authorization", fixture.create(String.class))) + .andExpect(status().isInternalServerError()) + .andExpect(result -> assertEquals(WRITING_CSV_RESULT_FAILED.toString(), result.getResponse().getContentAsString())); + } +}