Commit 38068c42 authored by calocedre TAC's avatar calocedre TAC
Browse files

fix slot computation

parent 22612a75
......@@ -8,7 +8,7 @@ import lombok.experimental.SuperBuilder;
import java.time.Instant;
@SuperBuilder
@SuperBuilder(toBuilder = true)
@Getter
@Setter
@ToString
......
......@@ -45,8 +45,8 @@ public class VisitExpositionAggregatorService implements IVisitExpositionAggrega
return;
}
int exposureTime = this.getExposureTime(visit.getVenueType(), visit.getVenueCategory1(), visit.getVenueCategory2(), visit.isStaff());
int firstExposedSlot = Math.max(0, (int) scanTimeSlot - exposureTime);
int lastExposedSlot = Math.min(this.getPeriodMaxSlot(visit.getPeriodDuration()), (int) scanTimeSlot + exposureTime);
int firstExposedSlot = Math.max(0, (int) scanTimeSlot - exposureTime + 1);
int lastExposedSlot = Math.min(this.getPeriodMaxSlot(visit.getPeriodDuration()), (int) scanTimeSlot + exposureTime - 1);
List<ExposedVisitEntity> exposedVisits = repository.findAllByLocationTemporaryPublicIdAndPeriodStart(visit.getLocationTemporaryPublicId(), this.periodStartFromCompressedPeriodStart(visit.getCompressedPeriodStartTime()));
......
package fr.gouv.clea.consumer.service.impl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import fr.gouv.clea.consumer.configuration.VenueConsumerConfiguration;
import fr.gouv.clea.consumer.model.ExposedVisitEntity;
import fr.gouv.clea.consumer.model.Visit;
import fr.gouv.clea.consumer.repository.IExposedVisitRepository;
import fr.inria.clea.lsp.utils.TimeUtils;
@ExtendWith(MockitoExtension.class)
public class SlotGenerationTest {
@Mock
private VenueConsumerConfiguration config;
@Mock
private IExposedVisitRepository repository;
@Captor
ArgumentCaptor<List<ExposedVisitEntity>> exposedVisitEntitiesCaptor;
@InjectMocks
@Spy
private VisitExpositionAggregatorService service;
private Instant todayAtMidnight;
private Instant todayAt8am;
private UUID uuid;
private byte[] locationTemporarySecretKey;
private byte[] encryptedLocationContactMessage;
@BeforeEach
void init() {
when(config.getDurationUnitInSeconds()).thenReturn((int) Duration.ofMinutes(30).toSeconds());
todayAtMidnight = Instant.now().truncatedTo(ChronoUnit.DAYS);
todayAt8am = todayAtMidnight.plus(8, ChronoUnit.HOURS);
uuid = UUID.randomUUID();
locationTemporarySecretKey = RandomUtils.nextBytes(20);
encryptedLocationContactMessage = RandomUtils.nextBytes(20);
}
@Test
@DisplayName("test how many slots are generated for a given visit with a period duration of 24 hour")
void testSlotGeneration() {
Instant todayAtMidnight = Instant.now().truncatedTo(ChronoUnit.DAYS);
Instant todayAt8am = todayAtMidnight.plus(8, ChronoUnit.HOURS);
Visit visit = defaultVisit().toBuilder()
.periodDuration(24)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAtMidnight))
.qrCodeValidityStartTime(todayAtMidnight)
.qrCodeScanTime(todayAt8am)
.build();
Mockito.doReturn(3).when(service).getExposureTime(Mockito.anyInt(), anyInt(), anyInt(), anyBoolean());
service.updateExposureCount(visit);
/* => scanTimeSlot = 8*2 = 16
* => slots to generate = 2 before + scanTimeSlot + 2 after = 5
* => firstExposedSlot = 16-2 = 14
* => lastExposedSlot = 16+2 = 18
*/
Mockito.verify(repository).saveAll(exposedVisitEntitiesCaptor.capture());
assertThat(exposedVisitEntitiesCaptor.getValue()).hasSize(5);
List<ExposedVisitEntity> entities = exposedVisitEntitiesCaptor.getValue();
List<Integer> expectedSlots = IntStream.rangeClosed(14, 18).boxed().collect(Collectors.toList());
assertThat(entities).extracting(ExposedVisitEntity::getTimeSlot).hasSameElementsAs(expectedSlots);
}
@Test
@DisplayName("test how many slots are generated for a visit at first slot with a period duration of 1 hour")
void testSlotGenerationDoesNotGoOverPeriodValidity() {
Visit visit = defaultVisit().toBuilder()
.periodDuration(1)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAt8am))
.qrCodeValidityStartTime(todayAt8am)
.qrCodeScanTime(todayAt8am)
.build();
service.updateExposureCount(visit);
/* => scanTimeSlot = 0
* => slots to generate = scanTimeSlot + 1 after = 2
* => firstExposedSlot = 0
* => lastExposedSlot = 0+1 = 1
*/
Mockito.verify(repository).saveAll(exposedVisitEntitiesCaptor.capture());
assertThat(exposedVisitEntitiesCaptor.getValue()).hasSize(2);
List<ExposedVisitEntity> entities = exposedVisitEntitiesCaptor.getValue();
assertThat(entities).extracting(ExposedVisitEntity::getTimeSlot).hasSameElementsAs(List.of(0, 1));
}
@Test
@DisplayName("test how many slots are generated for a visit at first slot with an unlimited period duration")
void testSlotGenerationWithUnlimitedPeriodDuration() {
Visit visit = defaultVisit().toBuilder()
.periodDuration(255)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAt8am))
.qrCodeValidityStartTime(todayAt8am)
.qrCodeScanTime(todayAt8am)
.build();
service.updateExposureCount(visit);
/*
* => scanTimeSlot = 0
* => slots to generate = scanTimeSlot + 2 after = 3
* => firstExposedSlot = 0
* => lastExposedSlot = 0+3-1 = 2
*/
Mockito.verify(repository).saveAll(exposedVisitEntitiesCaptor.capture());
assertThat(exposedVisitEntitiesCaptor.getValue()).hasSize(3);
List<ExposedVisitEntity> entities = exposedVisitEntitiesCaptor.getValue();
assertThat(entities).extracting(ExposedVisitEntity::getTimeSlot).hasSameElementsAs(List.of(0, 1, 2));
}
@Test
@DisplayName("no slot should be generated when qrScanTime is after period validity")
void testSlotGenerationWithQrScanTimeAfterPeriodValidity() {
Visit visit = defaultVisit().toBuilder()
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAtMidnight))
.periodDuration(6)
.qrCodeValidityStartTime(todayAtMidnight)
.qrCodeScanTime(todayAt8am)
.build();
service.updateExposureCount(visit);
Mockito.verify(repository, never()).saveAll(exposedVisitEntitiesCaptor.capture());
}
@Test
@DisplayName("test how many slots are generated for a visit at first slot when qrScanTime is after qr validity")
void testSlotGenerationWithQrScanTimeAfterQrValidity() {
// This case can happen with authorized drift
when(config.getDurationUnitInSeconds()).thenReturn((int) Duration.ofHours(1).toSeconds());
Visit visit = defaultVisit().toBuilder()
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAtMidnight))
.periodDuration(24)
.qrCodeValidityStartTime(todayAtMidnight)
.qrCodeRenewalIntervalExponentCompact(14) // 2^14 seconds = 4.55 hours
.qrCodeScanTime(todayAt8am)
.build();
service.updateExposureCount(visit);
/*
* => scanTimeSlot = 8
* => slots to generate = scanTimeSlot + 2 after + 2 before
* => firstExposedSlot = 10
* => lastExposedSlot = 6
*/
Mockito.verify(repository).saveAll(exposedVisitEntitiesCaptor.capture());
assertThat(exposedVisitEntitiesCaptor.getValue()).hasSize(5);
List<ExposedVisitEntity> entities = exposedVisitEntitiesCaptor.getValue();
assertThat(entities).extracting(ExposedVisitEntity::getTimeSlot).hasSameElementsAs(List.of(6, 7, 8, 9, 10));
}
protected Visit defaultVisit() {
return Visit.builder()
.version(0)
.type(0)
.staff(true)
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(1)
.venueCategory2(1)
.periodDuration(24)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAtMidnight))
.qrCodeValidityStartTime(Instant.now())
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAt8am)
.isBackward(true)
.build();
}
protected int getCompressedPeriodStartTime(Instant instant) {
return (int) (TimeUtils.ntpTimestampFromInstant(instant) / 3600);
}
}
package fr.gouv.clea.consumer.service.impl;
import fr.gouv.clea.consumer.model.ExposedVisitEntity;
import fr.gouv.clea.consumer.model.Visit;
import fr.gouv.clea.consumer.repository.IExposedVisitRepository;
import fr.gouv.clea.consumer.service.IVisitExpositionAggregatorService;
import fr.inria.clea.lsp.utils.TimeUtils;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
......@@ -14,13 +16,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
import fr.gouv.clea.consumer.model.ExposedVisitEntity;
import fr.gouv.clea.consumer.model.Visit;
import fr.gouv.clea.consumer.repository.IExposedVisitRepository;
import fr.gouv.clea.consumer.service.IVisitExpositionAggregatorService;
import fr.inria.clea.lsp.utils.TimeUtils;
@SpringBootTest
@DirtiesContext
......@@ -34,7 +34,6 @@ class VisitExpositionAggregatorServiceTest {
private Instant todayAtMidnight;
private Instant todayAt8am;
private long todayAtMidnightAsNtp;
private UUID uuid;
private byte[] locationTemporarySecretKey;
private byte[] encryptedLocationContactMessage;
......@@ -43,7 +42,6 @@ class VisitExpositionAggregatorServiceTest {
void init() {
todayAtMidnight = Instant.now().truncatedTo(ChronoUnit.DAYS);
todayAt8am = todayAtMidnight.plus(8, ChronoUnit.HOURS);
todayAtMidnightAsNtp = TimeUtils.ntpTimestampFromInstant(todayAtMidnight);
uuid = UUID.randomUUID();
locationTemporarySecretKey = RandomUtils.nextBytes(20);
encryptedLocationContactMessage = RandomUtils.nextBytes(20);
......@@ -57,23 +55,11 @@ class VisitExpositionAggregatorServiceTest {
@Test
@DisplayName("visits with no existing context should be saved in DB")
void saveWithNoContext() {
Visit visit = Visit.builder()
.version(0)
.type(0)
.staff(true)
Visit visit = defaultVisit().toBuilder()
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(0)
.venueCategory2(0)
.periodDuration(24)
.compressedPeriodStartTime((int) (todayAtMidnightAsNtp / 3600))
.qrCodeValidityStartTime(Instant.now())
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAt8am)
.isBackward(true)
.build();
service.updateExposureCount(visit);
List<ExposedVisitEntity> entities = repository.findAll();
......@@ -87,31 +73,17 @@ class VisitExpositionAggregatorServiceTest {
@Test
@DisplayName("visits with existing context should be updated in DB")
void updateWithExistingContext() {
Visit visit = Visit.builder()
.version(0)
.type(0)
.staff(true)
Visit visit = defaultVisit().toBuilder()
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(0)
.venueCategory2(0)
.periodDuration(24)
.compressedPeriodStartTime((int) (todayAtMidnightAsNtp / 3600))
.qrCodeValidityStartTime(Instant.now())
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAt8am)
.isBackward(true)
.build();
service.updateExposureCount(visit);
long before = repository.count();
service.updateExposureCount(visit);
long after = repository.count();
assertThat(before).isEqualTo(after);
List<ExposedVisitEntity> entities = repository.findAll();
entities.forEach(it -> {
assertThat(it.getLocationTemporaryPublicId()).isEqualTo(uuid);
......@@ -123,32 +95,20 @@ class VisitExpositionAggregatorServiceTest {
@Test
@DisplayName("new visits should be saved while existing be updated in DB")
void mixedContext() {
Visit visit = Visit.builder()
.version(0)
.type(0)
.staff(true)
Visit visit = defaultVisit().toBuilder()
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(0)
.venueCategory2(0)
.periodDuration(24)
.compressedPeriodStartTime((int) (todayAtMidnightAsNtp / 3600))
.qrCodeValidityStartTime(Instant.now())
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAt8am)
.isBackward(true)
.build();
service.updateExposureCount(visit);
visit.setBackward(false);
service.updateExposureCount(visit);
UUID newUUID = UUID.randomUUID();
visit.setLocationTemporaryPublicId(newUUID);
visit.setBackward(true);
Visit visit2 = visit.toBuilder()
.locationTemporaryPublicId(newUUID)
.isBackward(true)
.build();
service.updateExposureCount(visit);
service.updateExposureCount(visit2);
List<ExposedVisitEntity> entities = repository.findAll();
entities.stream()
......@@ -159,7 +119,6 @@ class VisitExpositionAggregatorServiceTest {
assertThat(it.getForwardVisits()).isEqualTo(1);
}
);
entities.stream()
.filter(it -> it.getLocationTemporaryPublicId().equals(newUUID))
.forEach(it -> {
......@@ -170,72 +129,16 @@ class VisitExpositionAggregatorServiceTest {
);
}
@Test
@DisplayName("test how many slots are generated for a given visit with a period duration of 24 hour")
void testSlotGeneration() {
Instant todayAtMidnight = Instant.now().truncatedTo(ChronoUnit.DAYS);
Instant todayAt8am = todayAtMidnight.plus(8, ChronoUnit.HOURS);
Visit visit = Visit.builder()
.version(0)
.type(0)
.staff(true)
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(0)
.venueCategory2(0)
.periodDuration(24)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAtMidnight))
.qrCodeValidityStartTime(todayAtMidnight)
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAt8am)
.isBackward(true)
.build();
service.updateExposureCount(visit);
/*
* if:
* periodDuration = 24 hours
* periodStartTime = today at 00:00:00
* qrCodeScanTime = today at 08:00:00
* durationUnit = 1800 seconds
* exposureTime = 3
*
* then:
* => scanTimeSlot = 8*2 = 16
* => slots to generate = 3 before + scanTimeSlot + 3 after = 7
* => firstExposedSlot = 16-3 = 13
* => lastExposedSlot = 16+3 = 19
*/
assertThat(repository.count()).isEqualTo(7L);
List<ExposedVisitEntity> entities = repository.findAll();
IntStream.rangeClosed(13, 19)
.forEach(slot -> assertThat(entities.stream().filter(it -> it.getTimeSlot() == slot).count()).isEqualTo(1));
}
@Test
@DisplayName("stop processing if qrCodeScanTime is before periodStartTime")
void testWhenQrScanIsBeforePeriodStart() {
Instant todayAtMidnight = Instant.now().truncatedTo(ChronoUnit.DAYS);
Instant todayAt8am = todayAtMidnight.plus(8, ChronoUnit.HOURS);
Visit visit = Visit.builder()
.version(0)
.type(0)
.staff(true)
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(0)
.venueCategory2(0)
Visit visit = defaultVisit().toBuilder()
.periodDuration(24)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAt8am))
.qrCodeValidityStartTime(todayAt8am)
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAtMidnight)
.isBackward(true)
.build();
service.updateExposureCount(visit);
......@@ -243,136 +146,24 @@ class VisitExpositionAggregatorServiceTest {
assertThat(repository.count()).isZero();
}
@Test
@DisplayName("test how many slots are generated for a given visit with a period duration of 1 hour")
void testSlotGenerationWithPeriodDuration1() {
Instant todayAtMidnight = Instant.now().truncatedTo(ChronoUnit.DAYS);
Instant todayAt8am = todayAtMidnight.plus(8, ChronoUnit.HOURS);
Visit visit = Visit.builder()
.version(0)
.type(0)
.staff(true)
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(0)
.venueCategory2(0)
.periodDuration(1)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAt8am))
.qrCodeValidityStartTime(todayAt8am)
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAt8am)
.isBackward(true)
.build();
service.updateExposureCount(visit);
/*
* if:
* periodDuration = 1 hours
* periodStartTime = today at 08:00:00
* qrCodeScanTime = today at 08:00:00
* durationUnit = 1800 seconds
* exposureTime = 3
*
* then:
* => scanTimeSlot = 0
* => slots to generate = scanTimeSlot + 1 after = 2
* => firstExposedSlot = 0
* => lastExposedSlot = 0+1 = 1
*/
assertThat(repository.count()).isEqualTo(2L);
List<ExposedVisitEntity> entities = repository.findAll();
IntStream.rangeClosed(0, 1)
.forEach(step -> assertThat(entities.stream().filter(it -> it.getTimeSlot() == step).count()).isEqualTo(1));
}
@Test
@DisplayName("test how many slots are generated for a given visit with a period duration of 255 hour")
void testSlotGenerationWithPeriodDuration255() {
Instant todayAtMidnight = Instant.now().truncatedTo(ChronoUnit.DAYS);
Instant todayAt8am = todayAtMidnight.plus(8, ChronoUnit.HOURS);
Visit visit = Visit.builder()
protected Visit defaultVisit() {
return Visit.builder()
.version(0)
.type(0)
.staff(true)
.locationTemporaryPublicId(uuid)
.qrCodeRenewalIntervalExponentCompact(2)
.venueType(4)
.venueCategory1(0)
.venueCategory2(0)
.periodDuration(255)
.compressedPeriodStartTime(getCompressedPeriodStartTime(todayAt8am))
.qrCodeValidityStartTime(todayAt8am)
.locationTemporarySecretKey(locationTemporarySecretKey)
.encryptedLocationContactMessage(encryptedLocationContactMessage)
.qrCodeScanTime(todayAt8am)
.isBackward(true)
.build();
service.updateExposureCount(visit);
/*
* if:
* periodDuration = 255 hours
* periodStartTime = today at 08:00:00
* qrCodeScanTime = today at 08:00:00
* durationUnit = 1800 seconds
* exposureTime = 3
*
* then:
* => scanTimeSlot = 0
* => slots to generate = scanTimeSlot + 2 after = 3
* => firstExposedSlot = 0
* => lastExposedSlot = 0+3-1 = 2
*/
assertThat(repository.count()).isEqualTo(4L);
List<ExposedVisitEntity> entities = repository.findAll();
entities.forEach(it -> System.out.println(it.getTimeSlot()));
IntStream.rangeClosed(0, 2)
.forEach(step -> assertThat(entities.stream().filter(it -> it.getTimeSlot() == step).count()).isEqualTo(1));