From 42bc2bcc9d8608e5cf81b86db50ef94b5b1eab88 Mon Sep 17 00:00:00 2001 From: nbrouand <7816908+nbrouand@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:07:47 +0100 Subject: [PATCH] feat(server+ui): Schedule campaign: overload environment and dataset (#191) --- .../SchedulingConfiguration.java | 5 - .../api/ScheduleCampaignController.java | 19 +- .../api/dto/SchedulingCampaignDto.java | 60 +++- .../domain/PeriodicScheduledCampaign.java | 47 ++- ....java => ScheduledCampaignRepository.java} | 2 +- .../infra/DatabaseCampaignRepository.java | 10 +- .../campaign/infra/SchedulingCampaignDto.java | 43 ++- .../SchedulingCampaignFileRepository.java | 21 +- .../SchedulingCampaignsDtoDeserializer.java | 65 ++-- .../campaign/CampaignExecutionEngine.java | 26 +- .../domain/schedule/CampaignScheduler.java | 42 +-- ...edControllerSpringBootIntegrationTest.java | 5 +- .../api/ScheduleCampaignControllerTest.java | 141 ++++++++ ...icScheduledCampaignFileRepositoryTest.java | 275 --------------- .../ScheduledCampaignFileRepositoryTest.java | 315 ++++++++++++++++++ .../campaign/CampaignExecutionEngineTest.java | 31 +- .../schedule/CampaignSchedulerTest.java | 158 ++++----- .../src/test/java/util/AssertTestUtils.java | 22 ++ .../campaign/campaign-scheduling.model.ts | 22 +- .../services/campaign-scheduling.service.ts | 3 - .../campaign-list.component.html | 4 +- .../campaign-list/campaign-list.component.ts | 7 +- .../campaign-scheduling.component.html | 195 +++++++---- .../campaign-scheduling.component.scss | 33 +- .../campaign-scheduling.component.ts | 95 ++++-- .../campaign-edition.component.ts | 4 +- .../left-menu/chutney-left-menu.items.ts | 2 +- chutney/ui/src/assets/i18n/en.json | 14 +- chutney/ui/src/assets/i18n/fr.json | 12 +- example/.docker/demo/conf/chutney-demo.db | Bin 4096 -> 102400 bytes example/.docker/demo/conf/chutney-demo.db-shm | Bin 32768 -> 32768 bytes example/.docker/demo/conf/chutney-demo.db-wal | Bin 1120672 -> 4152 bytes 32 files changed, 1051 insertions(+), 627 deletions(-) rename chutney/server/src/main/java/com/chutneytesting/campaign/domain/{PeriodicScheduledCampaignRepository.java => ScheduledCampaignRepository.java} (88%) create mode 100644 chutney/server/src/test/java/com/chutneytesting/campaign/api/ScheduleCampaignControllerTest.java delete mode 100644 chutney/server/src/test/java/com/chutneytesting/campaign/infra/PeriodicScheduledCampaignFileRepositoryTest.java create mode 100644 chutney/server/src/test/java/com/chutneytesting/campaign/infra/ScheduledCampaignFileRepositoryTest.java create mode 100644 chutney/server/src/test/java/util/AssertTestUtils.java diff --git a/chutney/server/src/main/java/com/chutneytesting/SchedulingConfiguration.java b/chutney/server/src/main/java/com/chutneytesting/SchedulingConfiguration.java index 1a786a968..2563609b3 100644 --- a/chutney/server/src/main/java/com/chutneytesting/SchedulingConfiguration.java +++ b/chutney/server/src/main/java/com/chutneytesting/SchedulingConfiguration.java @@ -71,11 +71,6 @@ public TaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder builde return builder.threadNamePrefix("app-task-exec").build(); } - @Bean - public ApplicationRunner scheduledMissedCampaignToExecute(CampaignScheduler campaignScheduler) { - return arg -> campaignScheduler.scheduledMissedCampaignIds(); - } - /** * @see ScheduleCampaign#executeScheduledCampaign() */ diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/api/ScheduleCampaignController.java b/chutney/server/src/main/java/com/chutneytesting/campaign/api/ScheduleCampaignController.java index 57fc0c975..5b4969324 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/api/ScheduleCampaignController.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/api/ScheduleCampaignController.java @@ -7,11 +7,8 @@ package com.chutneytesting.campaign.api; -import static com.chutneytesting.campaign.domain.Frequency.toFrequency; - import com.chutneytesting.campaign.api.dto.SchedulingCampaignDto; -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaignRepository; +import com.chutneytesting.campaign.domain.ScheduledCampaignRepository; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -31,17 +28,17 @@ @CrossOrigin(origins = "*") public class ScheduleCampaignController { - private final PeriodicScheduledCampaignRepository periodicScheduledCampaignRepository; + private final ScheduledCampaignRepository scheduledCampaignRepository; - public ScheduleCampaignController(PeriodicScheduledCampaignRepository periodicScheduledCampaignRepository) { - this.periodicScheduledCampaignRepository = periodicScheduledCampaignRepository; + public ScheduleCampaignController(ScheduledCampaignRepository scheduledCampaignRepository) { + this.scheduledCampaignRepository = scheduledCampaignRepository; } @PreAuthorize("hasAuthority('CAMPAIGN_READ')") @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_VALUE) public List getAll() { - return periodicScheduledCampaignRepository.getAll().stream() - .map(sc -> new SchedulingCampaignDto(sc.id, sc.campaignsId, sc.campaignsTitle, sc.nextExecutionDate, sc.frequency.label)) + return scheduledCampaignRepository.getAll().stream() + .map(SchedulingCampaignDto::toDto) .sorted(Comparator.comparing(SchedulingCampaignDto::getSchedulingDate)) .collect(Collectors.toList()); } @@ -49,13 +46,13 @@ public List getAll() { @PreAuthorize("hasAuthority('CAMPAIGN_WRITE')") @PostMapping(path = "", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public void add(@RequestBody SchedulingCampaignDto dto) { - periodicScheduledCampaignRepository.add(new PeriodicScheduledCampaign(null, dto.getCampaignsId(), dto.getCampaignsTitle(), dto.getSchedulingDate(), toFrequency(dto.getFrequency()))); + scheduledCampaignRepository.add(SchedulingCampaignDto.fromDto(dto)); } @PreAuthorize("hasAuthority('CAMPAIGN_WRITE')") @DeleteMapping(path = "/{schedulingCampaignId}", produces = MediaType.APPLICATION_JSON_VALUE) public void delete(@PathVariable("schedulingCampaignId") Long schedulingCampaignId) { - periodicScheduledCampaignRepository.removeById(schedulingCampaignId); + scheduledCampaignRepository.removeById(schedulingCampaignId); } } diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/api/dto/SchedulingCampaignDto.java b/chutney/server/src/main/java/com/chutneytesting/campaign/api/dto/SchedulingCampaignDto.java index ad341827d..947be8284 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/api/dto/SchedulingCampaignDto.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/api/dto/SchedulingCampaignDto.java @@ -8,45 +8,48 @@ package com.chutneytesting.campaign.api.dto; +import static com.chutneytesting.campaign.domain.Frequency.toFrequency; + +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign.CampaignExecutionRequest; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public class SchedulingCampaignDto { private Long id; - private List campaignsId; - private List campaignsTitle; private LocalDateTime schedulingDate; private String frequency; + private String environment; + + private List campaignExecutionRequest = new ArrayList<>(); + @JsonCreator public SchedulingCampaignDto() { } public SchedulingCampaignDto(Long id, - List campaignsId, - List campaignsTitle, LocalDateTime schedulingDate, - String frequency + String frequency, + String environment, + List campaignExecutionRequest ) { this.id = id; - this.campaignsId = campaignsId; - this.campaignsTitle = campaignsTitle; this.schedulingDate = schedulingDate; this.frequency = frequency; + this.environment = environment; + this.campaignExecutionRequest = campaignExecutionRequest; } - public Long getId() { - return id; - } - - public List getCampaignsId() { - return campaignsId; + public record CampaignExecutionRequestDto(Long campaignId, String campaignTitle, String datasetId) { } - public List getCampaignsTitle() { - return campaignsTitle; + public Long getId() { + return id; } public LocalDateTime getSchedulingDate() { @@ -56,4 +59,31 @@ public LocalDateTime getSchedulingDate() { public String getFrequency() { return frequency; } + + public String getEnvironment() { + return environment; + } + + public List getCampaignExecutionRequest() { + return campaignExecutionRequest; + } + + public static SchedulingCampaignDto toDto(PeriodicScheduledCampaign sc) { + return new SchedulingCampaignDto(sc.id, + sc.nextExecutionDate, + sc.frequency.label, + sc.environment, + sc.campaignExecutionRequests.stream().map(cer -> new CampaignExecutionRequestDto(cer.campaignId(), cer.campaignTitle(), cer.datasetId())).toList() + ); + } + + public static PeriodicScheduledCampaign fromDto(SchedulingCampaignDto dto) { + return new PeriodicScheduledCampaign( + dto.id, + dto.getSchedulingDate(), + toFrequency(dto.getFrequency()), + dto.getEnvironment(), + dto.campaignExecutionRequest.stream().map(cer -> new CampaignExecutionRequest(cer.campaignId(), cer.campaignTitle(), cer.datasetId())).toList() + ); + } } diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/domain/PeriodicScheduledCampaign.java b/chutney/server/src/main/java/com/chutneytesting/campaign/domain/PeriodicScheduledCampaign.java index 7a94114ea..d5f0c28fd 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/domain/PeriodicScheduledCampaign.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/domain/PeriodicScheduledCampaign.java @@ -14,25 +14,31 @@ public class PeriodicScheduledCampaign { public final Long id; - public final List campaignsId; - public final List campaignsTitle; public final LocalDateTime nextExecutionDate; public final Frequency frequency; + public final String environment; + public final List campaignExecutionRequests; - public PeriodicScheduledCampaign(Long id, List campaignsId, List campaignsTitle, LocalDateTime nextExecutionDate, Frequency frequency) { + public PeriodicScheduledCampaign(Long id, LocalDateTime nextExecutionDate, Frequency frequency, String environment, List campaignExecutionRequests) { this.id = id; - this.campaignsId = campaignsId; - this.campaignsTitle = campaignsTitle; this.nextExecutionDate = nextExecutionDate; this.frequency = frequency; + this.environment = environment; + this.campaignExecutionRequests = campaignExecutionRequests; } - public PeriodicScheduledCampaign(Long id, List campaignsId, List campaignsTitle, LocalDateTime nextExecutionDate) { - this(id, campaignsId, campaignsTitle, nextExecutionDate, Frequency.EMPTY); + public PeriodicScheduledCampaign nextScheduledExecution() { + LocalDateTime scheduledDate = switch (this.frequency) { + case HOURLY -> this.nextExecutionDate.plusHours(1); + case DAILY -> this.nextExecutionDate.plusDays(1); + case WEEKLY -> this.nextExecutionDate.plusWeeks(1); + case MONTHLY -> this.nextExecutionDate.plusMonths(1); + default -> throw new IllegalStateException("Unexpected value: " + this.frequency); + }; + return new PeriodicScheduledCampaign(id, scheduledDate, frequency, environment, campaignExecutionRequests); } - public PeriodicScheduledCampaign(Long id, Long campaignId, String campaignTitle, LocalDateTime nextExecutionDate) { - this(id, List.of(campaignId), List.of(campaignTitle), nextExecutionDate, Frequency.EMPTY); + public record CampaignExecutionRequest(Long campaignId, String campaignTitle, String datasetId) { } @Override @@ -40,33 +46,26 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PeriodicScheduledCampaign that = (PeriodicScheduledCampaign) o; - return Objects.equals(id, that.id) && Objects.equals(campaignsId, that.campaignsId) && Objects.equals(campaignsTitle, that.campaignsTitle) && Objects.equals(nextExecutionDate, that.nextExecutionDate) && Objects.equals(frequency, that.frequency); + return Objects.equals(id, that.id) && + Objects.equals(nextExecutionDate, that.nextExecutionDate) && + Objects.equals(frequency, that.frequency) && + Objects.equals(environment, that.environment) && + Objects.equals(campaignExecutionRequests, that.campaignExecutionRequests); } @Override public int hashCode() { - return Objects.hash(id, campaignsId, campaignsTitle, nextExecutionDate, frequency); + return Objects.hash(id, nextExecutionDate, frequency, environment, campaignExecutionRequests); } @Override public String toString() { return "SchedulingCampaign{" + "id=" + id + - ", campaignId=" + campaignsId + - ", campaignTitle='" + campaignsTitle + '\'' + ", schedulingDate=" + nextExecutionDate + ", frequency='" + frequency + '\'' + + ", environment='" + environment + '\'' + + ", campaignExecutionRequests='" + campaignExecutionRequests + '\'' + '}'; } - - public PeriodicScheduledCampaign nextScheduledExecution() { - LocalDateTime scheduledDate = switch (this.frequency) { - case HOURLY -> this.nextExecutionDate.plusHours(1); - case DAILY -> this.nextExecutionDate.plusDays(1); - case WEEKLY -> this.nextExecutionDate.plusWeeks(1); - case MONTHLY -> this.nextExecutionDate.plusMonths(1); - default -> throw new IllegalStateException("Unexpected value: " + this.frequency); - }; - return new PeriodicScheduledCampaign(id, campaignsId, campaignsTitle, scheduledDate, frequency); - } } diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/domain/PeriodicScheduledCampaignRepository.java b/chutney/server/src/main/java/com/chutneytesting/campaign/domain/ScheduledCampaignRepository.java similarity index 88% rename from chutney/server/src/main/java/com/chutneytesting/campaign/domain/PeriodicScheduledCampaignRepository.java rename to chutney/server/src/main/java/com/chutneytesting/campaign/domain/ScheduledCampaignRepository.java index 07d37ddc7..c74cf3354 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/domain/PeriodicScheduledCampaignRepository.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/domain/ScheduledCampaignRepository.java @@ -12,7 +12,7 @@ /** * CRUD for SchedulingCampaign */ -public interface PeriodicScheduledCampaignRepository { +public interface ScheduledCampaignRepository { PeriodicScheduledCampaign add(PeriodicScheduledCampaign periodicScheduledCampaign); diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/DatabaseCampaignRepository.java b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/DatabaseCampaignRepository.java index 9c996f05c..cf28eda66 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/DatabaseCampaignRepository.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/DatabaseCampaignRepository.java @@ -14,7 +14,7 @@ import com.chutneytesting.campaign.domain.CampaignExecutionRepository; import com.chutneytesting.campaign.domain.CampaignNotFoundException; import com.chutneytesting.campaign.domain.CampaignRepository; -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaignRepository; +import com.chutneytesting.campaign.domain.ScheduledCampaignRepository; import com.chutneytesting.campaign.infra.jpa.CampaignEntity; import com.chutneytesting.campaign.infra.jpa.CampaignScenarioEntity; import com.chutneytesting.server.core.domain.scenario.campaign.Campaign; @@ -33,15 +33,15 @@ public class DatabaseCampaignRepository implements CampaignRepository { private final CampaignJpaRepository campaignJpaRepository; private final CampaignScenarioJpaRepository campaignScenarioJpaRepository; private final CampaignExecutionRepository campaignExecutionRepository; - private final PeriodicScheduledCampaignRepository periodicScheduledCampaignRepository; + private final ScheduledCampaignRepository scheduledCampaignRepository; public DatabaseCampaignRepository(CampaignJpaRepository campaignJpaRepository, CampaignScenarioJpaRepository campaignScenarioJpaRepository, - CampaignExecutionDBRepository campaignExecutionRepository, PeriodicScheduledCampaignRepository periodicScheduledCampaignRepository) { + CampaignExecutionDBRepository campaignExecutionRepository, ScheduledCampaignRepository scheduledCampaignRepository) { this.campaignJpaRepository = campaignJpaRepository; this.campaignScenarioJpaRepository = campaignScenarioJpaRepository; this.campaignExecutionRepository = campaignExecutionRepository; - this.periodicScheduledCampaignRepository = periodicScheduledCampaignRepository; + this.scheduledCampaignRepository = scheduledCampaignRepository; } @Override @@ -75,7 +75,7 @@ public boolean removeById(Long id) { if (campaignJpaRepository.existsById(id)) { campaignExecutionRepository.clearAllExecutionHistory(id); campaignJpaRepository.deleteById(id); - periodicScheduledCampaignRepository.removeCampaignId(id); + scheduledCampaignRepository.removeCampaignId(id); return true; } return false; diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignDto.java b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignDto.java index f1a881ffa..97a5ee459 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignDto.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignDto.java @@ -7,36 +7,61 @@ package com.chutneytesting.campaign.infra; +import static java.util.Collections.emptyList; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; import java.time.LocalDateTime; import java.util.List; +@JsonPropertyOrder({ "id", "schedulingDate", "environment", "campaignsId", "campaignsTitle", "datasetsId" }) public class SchedulingCampaignDto { public final String id; - public final List campaignsId; - public final List campaignsTitle; public final LocalDateTime schedulingDate; public final String frequency; + public final String environment; + + @JsonIgnore + public final List campaignExecutionRequestDto; /** * for ObjectMapper only **/ + @JsonCreator public SchedulingCampaignDto() { id = null; - campaignsId = null; schedulingDate = null; - campaignsTitle = null; frequency = null; + environment = null; + campaignExecutionRequestDto = null; } + @JsonIgnore public SchedulingCampaignDto(String id, - List campaignsId, - List campaignsTitle, LocalDateTime schedulingDate, - String frequency) { + String frequency, + String environment, + List campaignExecutionRequestDto) { this.id = id; - this.campaignsId = campaignsId; - this.campaignsTitle = campaignsTitle; this.schedulingDate = schedulingDate; this.frequency = frequency; + this.campaignExecutionRequestDto = campaignExecutionRequestDto; + this.environment = environment; + } + + public List getCampaignsId() { + return campaignExecutionRequestDto != null ? campaignExecutionRequestDto.stream().map(cer -> cer.campaignId).toList() : emptyList(); + } + + public List getCampaignsTitle() { + return campaignExecutionRequestDto != null ? campaignExecutionRequestDto.stream().map(cer -> cer.campaignTitle).toList() : emptyList(); + } + + public List getDatasetsId() { + return campaignExecutionRequestDto != null ? campaignExecutionRequestDto.stream().map(cer -> cer.datasetId).toList() : emptyList(); + } + + public record CampaignExecutionRequestDto(Long campaignId, String campaignTitle, String datasetId) { } } diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignFileRepository.java b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignFileRepository.java index 89ac50c48..ebfd25b99 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignFileRepository.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignFileRepository.java @@ -12,7 +12,9 @@ import static com.chutneytesting.tools.file.FileUtils.initFolder; import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaignRepository; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign.CampaignExecutionRequest; +import com.chutneytesting.campaign.domain.ScheduledCampaignRepository; +import com.chutneytesting.campaign.infra.SchedulingCampaignDto.CampaignExecutionRequestDto; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -27,6 +29,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -38,7 +41,7 @@ * Scheduling campaign persistence. */ @Repository -public class SchedulingCampaignFileRepository implements PeriodicScheduledCampaignRepository { +public class SchedulingCampaignFileRepository implements ScheduledCampaignRepository { private static final Path ROOT_DIRECTORY_NAME = Paths.get("scheduling"); private static final String SCHEDULING_CAMPAIGNS_FILE = "schedulingCampaigns.json"; @@ -99,12 +102,10 @@ public void removeCampaignId(Long id) { Map schedulingCampaigns = readFromDisk(); Map schedulingCampaignsFiltered = new HashMap<>(); schedulingCampaigns.forEach((key, schedulingCampaignDto) -> { - int indexCampaignId = schedulingCampaignDto.campaignsId.indexOf(id); - if (indexCampaignId != -1) { // Remove id and title if the campaignId has been found - schedulingCampaignDto.campaignsTitle.remove(indexCampaignId); - schedulingCampaignDto.campaignsId.remove(indexCampaignId); - } - if (!schedulingCampaignDto.campaignsId.isEmpty()) { // Set the schedule only if a campaign is present after removal + Optional foundRequest = schedulingCampaignDto.campaignExecutionRequestDto.stream().filter(cer -> cer.campaignId().equals(id)).findFirst(); + // Remove id and title if the campaignId has been found + foundRequest.ifPresent(schedulingCampaignDto.campaignExecutionRequestDto::remove); + if (!schedulingCampaignDto.campaignExecutionRequestDto.isEmpty()) { // Set the schedule only if a campaign is present after removal schedulingCampaignsFiltered.put(key, schedulingCampaignDto); } }); @@ -154,11 +155,11 @@ private void writeOnDisk(Path filePath, Map sched } private PeriodicScheduledCampaign fromDto(SchedulingCampaignDto dto) { - return new PeriodicScheduledCampaign(Long.valueOf(dto.id), dto.campaignsId, dto.campaignsTitle, dto.schedulingDate, toFrequency(dto.frequency)); + return new PeriodicScheduledCampaign(Long.valueOf(dto.id), dto.schedulingDate, toFrequency(dto.frequency), dto.environment, dto.campaignExecutionRequestDto.stream().map(aa -> new CampaignExecutionRequest(aa.campaignId(), aa.campaignTitle(), aa.datasetId())).toList()); } private SchedulingCampaignDto toDto(long id, PeriodicScheduledCampaign periodicScheduledCampaign) { - return new SchedulingCampaignDto(String.valueOf(id), periodicScheduledCampaign.campaignsId, periodicScheduledCampaign.campaignsTitle, periodicScheduledCampaign.nextExecutionDate, periodicScheduledCampaign.frequency.label); + return new SchedulingCampaignDto(String.valueOf(id), periodicScheduledCampaign.nextExecutionDate, periodicScheduledCampaign.frequency.label, periodicScheduledCampaign.environment, periodicScheduledCampaign.campaignExecutionRequests.stream().map(aa -> new CampaignExecutionRequestDto(aa.campaignId(), aa.campaignTitle(), aa.datasetId())).toList()); } private Long getCurrentMaxId(Map schedulingCampaigns) { diff --git a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignsDtoDeserializer.java b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignsDtoDeserializer.java index 299462d9a..bb43dcbef 100644 --- a/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignsDtoDeserializer.java +++ b/chutney/server/src/main/java/com/chutneytesting/campaign/infra/SchedulingCampaignsDtoDeserializer.java @@ -7,6 +7,10 @@ package com.chutneytesting.campaign.infra; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toCollection; + +import com.chutneytesting.campaign.infra.SchedulingCampaignDto.CampaignExecutionRequestDto; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; @@ -18,8 +22,11 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.StreamSupport; -// TODO : Implemented for retro compatibility - to remove in future version class SchedulingCampaignsDtoDeserializer extends StdDeserializer { protected SchedulingCampaignsDtoDeserializer() { @@ -31,40 +38,48 @@ public SchedulingCampaignDto deserialize(JsonParser parser, DeserializationConte JsonNode node = parser.readValueAsTree(); String id = node.get("id").asText(); - List campaignsId; - List campaignsTitle; LocalDateTime schedulingDate = getSchedulingDate(node); String frequency = getFrequency(node); + List campaignsId = getLongList(node.get("campaignsId")); + List campaignsTitle = getStringList(node.get("campaignsTitle")); + String environment = node.has("environment") ? node.get("environment").asText() : null; + List datasetIds = getStringList(node.get("datasetsId")); - if (node.has("campaignId")) { - campaignsId = List.of(node.get("campaignId").asLong()); - } else { - campaignsId = getCampaignsId(node.get("campaignsId")); - } + List campaignExecutionRequestDto = + IntStream.range(0, campaignsId.size()) + .mapToObj(i -> new CampaignExecutionRequestDto( + campaignsId.get(i), + campaignsTitle.get(i), + guardArrayOfBoundException(datasetIds, i)) + ) + .collect(Collectors.toList()); - if (node.has("campaignTitle")) { - campaignsTitle = List.of(node.get("campaignTitle").asText()); - } else { - campaignsTitle = getCampaignsTitle(node.get("campaignsTitle")); - } - return new SchedulingCampaignDto(id, campaignsId, campaignsTitle, schedulingDate, frequency); + return new SchedulingCampaignDto(id, schedulingDate, frequency, environment, campaignExecutionRequestDto); } - private List getCampaignsId(JsonNode node) { - List campaignsId = new ArrayList<>(); - for (JsonNode id : node) { - campaignsId.add(id.asLong()); + private String guardArrayOfBoundException(List list, int i) { + if (i < list.size()) { + return list.get(i); } - return campaignsId; + return ""; + } + + private List getLongList(JsonNode node) { + return extractList(node, JsonNode::asLong); + } + + private List getStringList(JsonNode node) { + return extractList(node, JsonNode::asText); } - private List getCampaignsTitle(JsonNode node) { - List campaignsTitle = new ArrayList<>(); - for (JsonNode title : node) { - campaignsTitle.add(title.asText()); + private List extractList(JsonNode node, Function mapper) { + if (node == null) { + return emptyList(); } - return campaignsTitle; + return StreamSupport.stream(node.spliterator(), false) + .map(mapper) + .collect(toCollection(ArrayList::new)); } private LocalDateTime getSchedulingDate(JsonNode node) throws JsonProcessingException { @@ -72,7 +87,7 @@ private LocalDateTime getSchedulingDate(JsonNode node) throws JsonProcessingExce return objectMapper.readValue(node.get("schedulingDate").toString(), LocalDateTime.class); } - private String getFrequency(JsonNode node) throws JsonProcessingException { + private String getFrequency(JsonNode node) { return node.has("frequency") ? node.get("frequency").asText() : null; } } diff --git a/chutney/server/src/main/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngine.java b/chutney/server/src/main/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngine.java index c032b8062..a5c5f2f6f 100644 --- a/chutney/server/src/main/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngine.java +++ b/chutney/server/src/main/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngine.java @@ -8,6 +8,7 @@ package com.chutneytesting.execution.domain.campaign; +import static com.chutneytesting.server.core.domain.dataset.DataSet.NO_DATASET; import static java.util.Collections.emptyList; import static java.util.Collections.singleton; import static java.util.Optional.ofNullable; @@ -83,7 +84,8 @@ public CampaignExecutionEngine(CampaignRepository campaignRepository, JiraXrayEmbeddedApi jiraXrayEmbeddedApi, ChutneyMetrics metrics, ExecutorService executorService, - DataSetRepository datasetRepository, ObjectMapper objectMapper) { + DataSetRepository datasetRepository, + ObjectMapper objectMapper) { this.campaignRepository = campaignRepository; this.campaignExecutionRepository = campaignExecutionRepository; this.scenarioExecutionEngine = scenarioExecutionEngine; @@ -117,12 +119,7 @@ public List executeByName(String campaignName, String environ public CampaignExecution executeById(Long campaignId, String environment, DataSet dataset, String userId) { return ofNullable(campaignRepository.findById(campaignId)) .map(campaign -> selectExecutionEnvironment(campaign, environment)) - .map(campaign -> { - if (dataset != null) { - campaign.executionDataset(null); - } - return executeScenarioInCampaign(campaign, userId, dataset); - }) + .map(campaign -> executeScenarioInCampaign(campaign, userId, dataset)) .orElseThrow(() -> new CampaignNotFoundException(campaignId)); } @@ -130,6 +127,15 @@ public CampaignExecution executeById(Long campaignId, String userId) { return executeById(campaignId, null, null, userId); } + public void executeScheduledCampaign(Long campaignId, String environment, String datasetId, String userId) { + if (datasetId != null) { + DataSet dataset = datasetRepository.findById(datasetId); //TODO need to manage DatasetNotFound ? + executeById(campaignId, environment, dataset, userId); + } else { + executeById(campaignId, environment, null, userId); + } + } + public Optional currentExecution(Long campaignId, String environment) { return campaignExecutionRepository.currentExecutions(campaignId) .stream() @@ -234,7 +240,7 @@ private CampaignExecution execute(Campaign campaign, CampaignExecution campaignE List testCaseDatasets = scenariosToExecute.stream() .map(cs -> testCaseRepository.findExecutableById(cs.scenarioId()) - .map(tc -> new TestCaseDataset(tc, resolveDataset(cs, campaignExecution))) + .map(tc -> new TestCaseDataset(tc, resolveScenarioDataset(cs, campaignExecution))) ) .filter(Optional::isPresent) .map(Optional::get) @@ -335,7 +341,7 @@ private ScenarioExecutionCampaign executeScenario(Campaign campaign, TestCaseDat return new ScenarioExecutionCampaign(testCaseDataset.testcase().id(), scenarioName, execution.summary()); } - private DataSet resolveDataset(Campaign.CampaignScenario campaignScenario, CampaignExecution campaignExecution) { + private DataSet resolveScenarioDataset(Campaign.CampaignScenario campaignScenario, CampaignExecution campaignExecution) { return ofNullable(campaignScenario.datasetId()) .map(datasetId -> DataSet.builder().withId(datasetId).withName("").build()) @@ -351,7 +357,7 @@ private DataSet resolveDataset(Campaign.CampaignScenario campaignScenario, Campa .withConstants(ds.constants) .build(); }) - .orElseGet(() -> DataSet.NO_DATASET); + .orElseGet(() -> NO_DATASET); } private ExecutionRequest buildExecutionRequest(Campaign campaign, TestCaseDataset testCaseDataset, CampaignExecution campaignExecution) { diff --git a/chutney/server/src/main/java/com/chutneytesting/execution/domain/schedule/CampaignScheduler.java b/chutney/server/src/main/java/com/chutneytesting/execution/domain/schedule/CampaignScheduler.java index ddedd4eda..3f5f3f54b 100644 --- a/chutney/server/src/main/java/com/chutneytesting/execution/domain/schedule/CampaignScheduler.java +++ b/chutney/server/src/main/java/com/chutneytesting/execution/domain/schedule/CampaignScheduler.java @@ -9,7 +9,8 @@ import com.chutneytesting.campaign.domain.Frequency; import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaignRepository; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign.CampaignExecutionRequest; +import com.chutneytesting.campaign.domain.ScheduledCampaignRepository; import com.chutneytesting.execution.domain.campaign.CampaignExecutionEngine; import java.time.Clock; import java.time.LocalDateTime; @@ -18,6 +19,7 @@ import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -31,19 +33,19 @@ public class CampaignScheduler { private static final Logger LOGGER = LoggerFactory.getLogger(CampaignScheduler.class); private final CampaignExecutionEngine campaignExecutionEngine; - private final PeriodicScheduledCampaignRepository periodicScheduledCampaignRepository; + private final ScheduledCampaignRepository scheduledCampaignRepository; private final Clock clock; private final ExecutorService executor; public CampaignScheduler( CampaignExecutionEngine campaignExecutionEngine, Clock clock, - PeriodicScheduledCampaignRepository periodicScheduledCampaignRepository, + ScheduledCampaignRepository scheduledCampaignRepository, @Qualifier("scheduledCampaignsExecutor") ExecutorService executor ) { this.campaignExecutionEngine = campaignExecutionEngine; this.clock = clock; - this.periodicScheduledCampaignRepository = periodicScheduledCampaignRepository; + this.scheduledCampaignRepository = scheduledCampaignRepository; this.executor = executor; } @@ -51,8 +53,8 @@ public CampaignScheduler( public void executeScheduledCampaigns() { try { executor.invokeAll( - scheduledCampaignIdsToExecute() - .map(this::executeScheduledCampaignById) + scheduledCampaignsToExecute() + .map(this::executeScheduledCampaign) .collect(Collectors.toList()) ); } catch (InterruptedException e) { @@ -60,32 +62,30 @@ public void executeScheduledCampaigns() { } } - private Callable executeScheduledCampaignById(List campaignsId) { + private Callable executeScheduledCampaign(Pair, String> executionRequests) { + String environment = executionRequests.getRight(); return () -> { - campaignsId.forEach(campaignId -> { + executionRequests.getLeft().forEach(executionRequest -> { try { - LOGGER.info("Execute campaign with id [{}]", campaignId); - campaignExecutionEngine.executeById(campaignId, SCHEDULER_EXECUTE_USER); + LOGGER.info("Execute campaign with id [{}]", executionRequest); + campaignExecutionEngine.executeScheduledCampaign(executionRequest.campaignId(), environment, executionRequest.datasetId(), SCHEDULER_EXECUTE_USER); } catch (Exception e) { - LOGGER.error("Error during campaign [{}] execution", campaignId, e); + LOGGER.error("Error during campaign [{}] execution", executionRequest, e); } }); return null; }; } - public void scheduledMissedCampaignIds() { - scheduledCampaignIdsToExecute().forEach(campaignId -> LOGGER.info("Reschedule missed campaign with id {}", campaignId)); - } - - synchronized private Stream> scheduledCampaignIdsToExecute() { + synchronized private Stream, String>> scheduledCampaignsToExecute() { try { - List all = periodicScheduledCampaignRepository.getAll(); + List all = scheduledCampaignRepository.getAll(); + LOGGER.info(all.toString()); return all.stream() .filter(sc -> sc.nextExecutionDate != null) .filter(sc -> sc.nextExecutionDate.isBefore(LocalDateTime.now(clock))) .peek(this::prepareScheduledCampaignForNextExecution) - .map(sc -> sc.campaignsId); + .map(sc -> Pair.of(sc.campaignExecutionRequests, sc.environment)); } catch (Exception e) { LOGGER.error("Error retrieving scheduled campaigns", e); return Stream.empty(); @@ -99,10 +99,10 @@ private void prepareScheduledCampaignForNextExecution(PeriodicScheduledCampaign while (periodicScheduledCampaignWithNextSchedule.nextExecutionDate.isBefore(LocalDateTime.now(clock))) { periodicScheduledCampaignWithNextSchedule = periodicScheduledCampaignWithNextSchedule.nextScheduledExecution(); } - periodicScheduledCampaignRepository.add(periodicScheduledCampaignWithNextSchedule); - LOGGER.info("Next execution of scheduled campaign(s) {} with frequency [{}] has been added", periodicScheduledCampaign.campaignsId, periodicScheduledCampaign.frequency); + scheduledCampaignRepository.add(periodicScheduledCampaignWithNextSchedule); + LOGGER.info("Next execution of scheduled campaign(s) {} with frequency [{}] has been added", periodicScheduledCampaign.campaignExecutionRequests, periodicScheduledCampaign.frequency); } - periodicScheduledCampaignRepository.removeById(periodicScheduledCampaign.id); + scheduledCampaignRepository.removeById(periodicScheduledCampaign.id); } catch (Exception e) { LOGGER.error("Error preparing scheduled campaign next execution [{}]", periodicScheduledCampaign.id, e); } diff --git a/chutney/server/src/test/java/blackbox/SecuredControllerSpringBootIntegrationTest.java b/chutney/server/src/test/java/blackbox/SecuredControllerSpringBootIntegrationTest.java index 1ad0d1324..7feea2452 100644 --- a/chutney/server/src/test/java/blackbox/SecuredControllerSpringBootIntegrationTest.java +++ b/chutney/server/src/test/java/blackbox/SecuredControllerSpringBootIntegrationTest.java @@ -113,7 +113,10 @@ private static Object[] securedEndPointList() { {GET, "/api/ui/campaign/v1/lastexecutions/20", "CAMPAIGN_READ", null, OK}, {GET, "/api/ui/campaign/v1/scenario/scenarioId", "SCENARIO_READ", null, OK}, {GET, "/api/ui/campaign/v1/scheduling", "CAMPAIGN_READ", null, OK}, - {POST, "/api/ui/campaign/v1/scheduling", "CAMPAIGN_WRITE", "{\"campaignsId\" : [ 1 ], \"campaignsTitle\" : [ \"cpg 1ta\" ], \"schedulingDate\" : [ 2024, 1, 30, 13, 1 ]}", OK}, + {POST, "/api/ui/campaign/v1/scheduling", "CAMPAIGN_WRITE", + """ + {"id":1,"schedulingDate":[2024,10,12,14,30,45],"frequency":"Daily","environment":"PROD","campaignsId":[1],"campaignsTitle":["title"],"datasetsId":["datasetId"]} + """, OK}, {DELETE, "/api/ui/campaign/v1/scheduling/666", "CAMPAIGN_WRITE", null, OK}, {GET, "/api/ui/campaign/execution/v1/campaignName", "CAMPAIGN_EXECUTE", null, OK}, diff --git a/chutney/server/src/test/java/com/chutneytesting/campaign/api/ScheduleCampaignControllerTest.java b/chutney/server/src/test/java/com/chutneytesting/campaign/api/ScheduleCampaignControllerTest.java new file mode 100644 index 000000000..d1dceb91b --- /dev/null +++ b/chutney/server/src/test/java/com/chutneytesting/campaign/api/ScheduleCampaignControllerTest.java @@ -0,0 +1,141 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 Enedis + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.chutneytesting.campaign.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.chutneytesting.RestExceptionHandler; +import com.chutneytesting.campaign.domain.Frequency; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign.CampaignExecutionRequest; +import com.chutneytesting.campaign.domain.ScheduledCampaignRepository; +import com.chutneytesting.server.core.domain.instrument.ChutneyMetrics; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +class ScheduleCampaignControllerTest { + + private MockMvc mockMvc; + + private final ScheduledCampaignRepository scheduledCampaignRepository = mock(ScheduledCampaignRepository.class); + + private final ScheduleCampaignController scheduleCampaignController = new ScheduleCampaignController(scheduledCampaignRepository); + + @BeforeEach + void setup() { + mockMvc = MockMvcBuilders.standaloneSetup(scheduleCampaignController) + .setControllerAdvice(new RestExceptionHandler(Mockito.mock(ChutneyMetrics.class))) + .build(); + } + + @Test + @WithMockUser(authorities = "CAMPAIGN_READ") + void should_get_all_scheduled_campaigns() throws Exception { + + CampaignExecutionRequest request = new CampaignExecutionRequest(1L, "title", "datasetId"); + when(scheduledCampaignRepository.getAll()).thenReturn(List.of( + new PeriodicScheduledCampaign(1L, LocalDateTime.of(2024, 10, 12, 14, 30, 45), Frequency.DAILY, "PROD", List.of(request)) + )); + + // Act & Assert + mockMvc.perform(get("/api/ui/campaign/v1/scheduling") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$[0].id").value(1L)) + .andExpect(jsonPath("$[0].frequency").value("Daily")) + .andExpect(jsonPath("$[0].environment").value("PROD")) + .andExpect(jsonPath("$[0].campaignExecutionRequest[0].campaignId").value(1L)) + .andExpect(jsonPath("$[0].campaignExecutionRequest[0].campaignTitle").value("title")) + .andExpect(jsonPath("$[0].campaignExecutionRequest[0].datasetId").value("datasetId")) + .andExpect(jsonPath("$[0].schedulingDate[0]").value(2024)) + .andExpect(jsonPath("$[0].schedulingDate[1]").value(10)) + .andExpect(jsonPath("$[0].schedulingDate[2]").value(12)) + .andExpect(jsonPath("$[0].schedulingDate[3]").value(14)) + .andExpect(jsonPath("$[0].schedulingDate[4]").value(30)) + .andExpect(jsonPath("$[0].schedulingDate[5]").value(45)); + + verify(scheduledCampaignRepository, times(1)).getAll(); + } + + @Test + @WithMockUser(authorities = "CAMPAIGN_WRITE") + void should_add_scheduled_campaign() throws Exception { + + mockMvc.perform(post("/api/ui/campaign/v1/scheduling") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "id":1, + "schedulingDate":[2024,10,12,14,30,45], + "frequency":"Daily", + "environment":"PROD", + "campaignExecutionRequest":[ + {"campaignId":1,"campaignTitle":"title","datasetId":"datasetId"} + ] + } + """)) + .andExpect(status().isOk()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PeriodicScheduledCampaign.class); + + + verify(scheduledCampaignRepository).add(captor.capture()); + + PeriodicScheduledCampaign capturedCampaign = captor.getValue(); + + assertThat(capturedCampaign) + .isNotNull() + .extracting("id", "nextExecutionDate", "frequency.label", "environment") + .containsExactly( + 1L, + LocalDateTime.of(2024, 10, 12, 14, 30, 45), + "Daily", + "PROD" + ); + + assertThat(capturedCampaign.campaignExecutionRequests) + .hasSize(1) + .first() + .extracting("campaignId", "campaignTitle", "datasetId") + .containsExactly(1L, "title", "datasetId"); + } + + @Test + @WithMockUser(authorities = "CAMPAIGN_WRITE") + void should_delete_scheduled_campaign() throws Exception { + Long schedulingCampaignId = 1L; + + mockMvc.perform(delete("/api/ui/campaign/v1/scheduling/{schedulingCampaignId}", schedulingCampaignId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + verify(scheduledCampaignRepository).removeById(schedulingCampaignId); + } +} diff --git a/chutney/server/src/test/java/com/chutneytesting/campaign/infra/PeriodicScheduledCampaignFileRepositoryTest.java b/chutney/server/src/test/java/com/chutneytesting/campaign/infra/PeriodicScheduledCampaignFileRepositoryTest.java deleted file mode 100644 index 9b1eb9ced..000000000 --- a/chutney/server/src/test/java/com/chutneytesting/campaign/infra/PeriodicScheduledCampaignFileRepositoryTest.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017-2024 Enedis - * - * SPDX-License-Identifier: Apache-2.0 - * - */ - -package com.chutneytesting.campaign.infra; - - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaignRepository; -import com.chutneytesting.tools.file.FileUtils; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.stream.IntStream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -public class PeriodicScheduledCampaignFileRepositoryTest { - - private static PeriodicScheduledCampaignRepository sut; - private static Path SCHEDULING_CAMPAIGN_FILE; - @TempDir - private static Path temporaryFolder; - - @BeforeEach - public void setup() { - String tmpConfDir = temporaryFolder.toFile().getAbsolutePath(); - SCHEDULING_CAMPAIGN_FILE = Paths.get(tmpConfDir + "/scheduling/schedulingCampaigns.json"); - sut = new SchedulingCampaignFileRepository(tmpConfDir); - } - - @Test - public void should_add_get_and_remove_scheduled_campaign() { - //// ADD - // Given - PeriodicScheduledCampaign sc1 = new PeriodicScheduledCampaign(null, 11L, "campaign title 1", LocalDateTime.of(2020, 2, 4, 7, 10)); - PeriodicScheduledCampaign sc2 = new PeriodicScheduledCampaign(null, 22L, "campaign title 2", LocalDateTime.of(2021, 3, 5, 8, 11)); - PeriodicScheduledCampaign sc3 = new PeriodicScheduledCampaign(null, 33L, "campaign title 3", LocalDateTime.of(2022, 4, 6, 9, 12)); - PeriodicScheduledCampaign sc4 = new PeriodicScheduledCampaign(null, List.of(55L, 66L), List.of("campaign title 5", "campaign title 6"), LocalDateTime.of(2022, 4, 6, 9, 12)); - String expectedAdded = - """ - { - "1" : { - "id" : "1", - "campaignsId" : [ 11 ], - "campaignsTitle" : [ "campaign title 1" ], - "schedulingDate" : [ 2020, 2, 4, 7, 10 ] - }, - "2" : { - "id" : "2", - "campaignsId" : [ 22 ], - "campaignsTitle" : [ "campaign title 2" ], - "schedulingDate" : [ 2021, 3, 5, 8, 11 ] - }, - "3" : { - "id" : "3", - "campaignsId" : [ 33 ], - "campaignsTitle" : [ "campaign title 3" ], - "schedulingDate" : [ 2022, 4, 6, 9, 12 ] - }, - "4" : { - "id" : "4", - "campaignsId" : [ 55, 66 ], - "campaignsTitle" : [ "campaign title 5", "campaign title 6" ], - "schedulingDate" : [ 2022, 4, 6, 9, 12 ] - } - } - """; - - // When - sut.add(sc1); - sut.add(sc2); - sut.add(sc3); - sut.add(sc4); - - // Then - String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); - assertThat(actualContent).isEqualToIgnoringNewLines(expectedAdded); - - //// REMOVE - // Given - String expectedAfterRemove = - """ - { - "1" : { - "id" : "1", - "campaignsId" : [ 11 ], - "campaignsTitle" : [ "campaign title 1" ], - "schedulingDate" : [ 2020, 2, 4, 7, 10 ] - }, - "3" : { - "id" : "3", - "campaignsId" : [ 33 ], - "campaignsTitle" : [ "campaign title 3" ], - "schedulingDate" : [ 2022, 4, 6, 9, 12 ] - } - } - """; - - // When - sut.removeById(2L); - sut.removeById(4L); - - // Then - actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); - assertThat(actualContent).isEqualToIgnoringNewLines(expectedAfterRemove); - - //// GET - // When - List periodicScheduledCampaigns = sut.getAll(); - - // Then - assertThat(periodicScheduledCampaigns).hasSize(2); - PeriodicScheduledCampaign sc1WithId = new PeriodicScheduledCampaign(1L, 11L, "campaign title 1", LocalDateTime.of(2020, 2, 4, 7, 10)); - PeriodicScheduledCampaign sc3WithId = new PeriodicScheduledCampaign(3L, 33L, "campaign title 3", LocalDateTime.of(2022, 4, 6, 9, 12)); - - assertThat(periodicScheduledCampaigns).contains(sc1WithId, sc3WithId); - } - - @Test - public void should_remove_campaign_from_scheduled() { - // Given - FileUtils.writeContent(SCHEDULING_CAMPAIGN_FILE, "{}"); - PeriodicScheduledCampaign periodicScheduledCampaign = new PeriodicScheduledCampaign(null, List.of(1L, 2L, 3L), List.of("campaign title 1","campaign title 2","campaign title 3"), LocalDateTime.of(2024, 2, 4, 7, 10)); - sut.add(periodicScheduledCampaign); - - String expectedAdded = - """ - { - "1" : { - "id" : "1", - "campaignsId" : [ 1, 3 ], - "campaignsTitle" : [ "campaign title 1", "campaign title 3" ], - "schedulingDate" : [ 2024, 2, 4, 7, 10 ] - } - } - """; - - // When - sut.removeCampaignId(2L); - - // Then - String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); - assertThat(actualContent).isEqualToIgnoringNewLines(expectedAdded); - } - - @Test - public void should_remove_schedule_without_campaign_after_removing_campaign() { - // Given - FileUtils.writeContent(SCHEDULING_CAMPAIGN_FILE, "{}"); - PeriodicScheduledCampaign sc1 = new PeriodicScheduledCampaign(null, 11L, "campaign title 1", LocalDateTime.of(2020, 2, 4, 7, 10)); - PeriodicScheduledCampaign sc2 = new PeriodicScheduledCampaign(null, 22L, "campaign title 2", LocalDateTime.of(2021, 3, 5, 8, 11)); - sut.add(sc1); - sut.add(sc2); - String expectedAdded = - """ - { - "2" : { - "id" : "2", - "campaignsId" : [ 22 ], - "campaignsTitle" : [ "campaign title 2" ], - "schedulingDate" : [ 2021, 3, 5, 8, 11 ] - } - } - """; - - // When - sut.removeCampaignId(11L); - - // Then - String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); - assertThat(actualContent).isEqualToIgnoringNewLines(expectedAdded); - } - - @Test - public void should_get_and_update_old_scheduled_campaign() { - //// Get - // Given - String old_scheduled_campaign = - """ - { - "1" : { - "id" : "1", - "campaignId" : 11 , - "campaignTitle" : "campaign title 1", - "schedulingDate" : [ 2020, 2, 4, 7, 10 ] - } - } - """; - - FileUtils.writeContent(SCHEDULING_CAMPAIGN_FILE, old_scheduled_campaign); - - PeriodicScheduledCampaign sc1 = new PeriodicScheduledCampaign(1L, 11L, "campaign title 1", LocalDateTime.of(2020, 2, 4, 7, 10)); - PeriodicScheduledCampaign sc2 = new PeriodicScheduledCampaign(2L, 22L, "campaign title 2", LocalDateTime.of(2023, 3, 4, 7, 10)); - - // When - List periodicScheduledCampaigns = sut.getAll(); - //Then - assertThat(periodicScheduledCampaigns).hasSize(1); - assertThat(periodicScheduledCampaigns).contains(sc1); - - //// UPDATE - // When - sut.add(sc2); - - // Then - periodicScheduledCampaigns = sut.getAll(); - assertThat(periodicScheduledCampaigns).hasSize(2); - assertThat(periodicScheduledCampaigns).contains(sc1); - assertThat(periodicScheduledCampaigns).contains(sc2); - - String expectedScheduledCampaignsAfterUpdate = - """ - { - "1" : { - "id" : "1", - "campaignsId" : [ 11 ], - "campaignsTitle" : [ "campaign title 1" ], - "schedulingDate" : [ 2020, 2, 4, 7, 10 ] - }, - "2" : { - "id" : "2", - "campaignsId" : [ 22 ], - "campaignsTitle" : [ "campaign title 2" ], - "schedulingDate" : [ 2023, 3, 4, 7, 10 ] - } - } - """; - String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); - assertThat(actualContent).isEqualToIgnoringNewLines(expectedScheduledCampaignsAfterUpdate); - } - - @Test - void should_read_and_write_concurrently() throws InterruptedException { - List exceptions = new ArrayList<>(); - Runnable addScheduledCampaign = () -> { - try { - sut.add(new PeriodicScheduledCampaign(null, 11L, "campaign title 1", LocalDateTime.now().minusWeeks(1))); - } catch (Exception e) { - exceptions.add(e); - } - }; - Runnable readScheduledCampaigns = () -> { - try { - sut.getAll(); - } catch (Exception e) { - exceptions.add(e); - } - }; - - ExecutorService pool = Executors.newFixedThreadPool(2); - IntStream.range(1, 5).forEach((i) -> { - pool.submit(addScheduledCampaign); - pool.submit(readScheduledCampaigns); - }); - pool.shutdown(); - if (pool.awaitTermination(5, TimeUnit.SECONDS)) { - assertThat(exceptions).isEmpty(); - } else { - fail("Pool termination timeout ..."); - } - } -} diff --git a/chutney/server/src/test/java/com/chutneytesting/campaign/infra/ScheduledCampaignFileRepositoryTest.java b/chutney/server/src/test/java/com/chutneytesting/campaign/infra/ScheduledCampaignFileRepositoryTest.java new file mode 100644 index 000000000..58125876c --- /dev/null +++ b/chutney/server/src/test/java/com/chutneytesting/campaign/infra/ScheduledCampaignFileRepositoryTest.java @@ -0,0 +1,315 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 Enedis + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.chutneytesting.campaign.infra; + + +import static java.time.LocalDateTime.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.chutneytesting.campaign.domain.Frequency; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign.CampaignExecutionRequest; +import com.chutneytesting.campaign.domain.ScheduledCampaignRepository; +import com.chutneytesting.tools.file.FileUtils; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class ScheduledCampaignFileRepositoryTest { + + private static ScheduledCampaignRepository sut; + private static Path SCHEDULING_CAMPAIGN_FILE; + @TempDir + private static Path temporaryFolder; + + @BeforeEach + void setup() { + String tmpConfDir = temporaryFolder.toFile().getAbsolutePath(); + SCHEDULING_CAMPAIGN_FILE = Paths.get(tmpConfDir + "/scheduling/schedulingCampaigns.json"); + sut = new SchedulingCampaignFileRepository(tmpConfDir); + FileUtils.writeContent(SCHEDULING_CAMPAIGN_FILE, "{}"); + } + + @Test + void add_get_and_remove_scheduled_campaign() { + //// ADD + // Given + PeriodicScheduledCampaign sc1 = create(11L, "campaign title 1", of(2020, 2, 4, 7, 10)); + PeriodicScheduledCampaign sc2 = create(22L, "campaign title 2", of(2021, 3, 5, 8, 11)); + PeriodicScheduledCampaign sc3 = create(33L, "campaign title 3", of(2022, 4, 6, 9, 12)); + PeriodicScheduledCampaign sc4 = create(null, List.of(55L, 66L), List.of("campaign title 5", "campaign title 6"), of(2022, 4, 6, 9, 12), null, null); + String expectedAdded = + """ + { + "1" : { + "id" : "1", + "schedulingDate" : [ 2020, 2, 4, 7, 10 ], + "campaignsId" : [ 11 ], + "campaignsTitle" : [ "campaign title 1" ], + "datasetsId" : [ "" ] + }, + "2" : { + "id" : "2", + "schedulingDate" : [ 2021, 3, 5, 8, 11 ], + "campaignsId" : [ 22 ], + "campaignsTitle" : [ "campaign title 2" ], + "datasetsId" : [ "" ] + }, + "3" : { + "id" : "3", + "schedulingDate" : [ 2022, 4, 6, 9, 12 ], + "campaignsId" : [ 33 ], + "campaignsTitle" : [ "campaign title 3" ], + "datasetsId" : [ "" ] + }, + "4" : { + "id" : "4", + "schedulingDate" : [ 2022, 4, 6, 9, 12 ], + "campaignsId" : [ 55, 66 ], + "campaignsTitle" : [ "campaign title 5", "campaign title 6" ], + "datasetsId" : [ "", "" ] + } + } + """; + + // When + sut.add(sc1); + sut.add(sc2); + sut.add(sc3); + sut.add(sc4); + + // Then + String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); + assertThat(actualContent).isEqualToIgnoringNewLines(expectedAdded); + + //// REMOVE + // Given + String expectedAfterRemove = + """ + { + "1" : { + "id" : "1", + "schedulingDate" : [ 2020, 2, 4, 7, 10 ], + "campaignsId" : [ 11 ], + "campaignsTitle" : [ "campaign title 1" ], + "datasetsId" : [ "" ] + }, + "3" : { + "id" : "3", + "schedulingDate" : [ 2022, 4, 6, 9, 12 ], + "campaignsId" : [ 33 ], + "campaignsTitle" : [ "campaign title 3" ], + "datasetsId" : [ "" ] + } + } + """; + + // When + sut.removeById(2L); + sut.removeById(4L); + + // Then + actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); + assertThat(actualContent).isEqualToIgnoringWhitespace(expectedAfterRemove); + + //// GET + // When + List periodicScheduledCampaigns = sut.getAll(); + + // Then + assertThat(periodicScheduledCampaigns).hasSize(2); + PeriodicScheduledCampaign sc1WithId = create(1L, 11L, "campaign title 1", of(2020, 2, 4, 7, 10)); + PeriodicScheduledCampaign sc3WithId = create(3L, 33L, "campaign title 3", of(2022, 4, 6, 9, 12)); + + assertThat(periodicScheduledCampaigns).contains(sc1WithId, sc3WithId); + } + + @Test + void remove_campaign_from_scheduled() { + // Given + PeriodicScheduledCampaign periodicScheduledCampaign = create(null, List.of(1L, 2L, 3L), List.of("campaign title 1", "campaign title 2", "campaign title 3"), of(2024, 2, 4, 7, 10), null, null); + sut.add(periodicScheduledCampaign); + + String expectedAdded = + """ + { + "1" : { + "id" : "1", + "schedulingDate" : [ 2024, 2, 4, 7, 10 ], + "campaignsId" : [ 1, 3 ], + "campaignsTitle" : [ "campaign title 1", "campaign title 3" ], + "datasetsId" : [ "", "" ] + } + } + """; + + // When + sut.removeCampaignId(2L); + + // Then + String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); + assertThat(actualContent).isEqualToIgnoringNewLines(expectedAdded); + } + + @Test + void remove_schedule_without_campaign_after_removing_campaign() { + // Given + PeriodicScheduledCampaign sc1 = create(11L, "campaign title 1", of(2020, 2, 4, 7, 10)); + PeriodicScheduledCampaign sc2 = create(22L, "campaign title 2", of(2021, 3, 5, 8, 11)); + sut.add(sc1); + sut.add(sc2); + String expectedAdded = + """ + { + "2" : { + "id" : "2", + "schedulingDate" : [ 2021, 3, 5, 8, 11 ], + "campaignsId" : [ 22 ], + "campaignsTitle" : [ "campaign title 2" ], + "datasetsId" : [ "" ] + } + } + """; + + // When + sut.removeCampaignId(11L); + + // Then + String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); + assertThat(actualContent).isEqualToIgnoringWhitespace(expectedAdded); + } + + @Test + void should_read_and_write_scheduled_campaign_concurrently() throws InterruptedException { + List exceptions = new ArrayList<>(); + Runnable addScheduledCampaign = () -> { + try { + sut.add(create(11L, "campaign title 1", LocalDateTime.now().minusWeeks(1))); + } catch (Exception e) { + exceptions.add(e); + } + }; + Runnable readScheduledCampaigns = () -> { + try { + sut.getAll(); + } catch (Exception e) { + exceptions.add(e); + } + }; + + ExecutorService pool = Executors.newFixedThreadPool(2); + IntStream.range(1, 5).forEach((i) -> { + pool.submit(addScheduledCampaign); + pool.submit(readScheduledCampaigns); + }); + pool.shutdown(); + if (pool.awaitTermination(1, TimeUnit.SECONDS)) { + assertThat(exceptions).isEmpty(); + } else { + fail("Pool termination timeout ..."); + } + } + + @Test + public void should_get_and_update_old_scheduled_campaign() { + //// Get + // Given + String old_scheduled_campaign = + """ + { + "1" : { + "id" : "1", + "campaignsId" : [ 11 ], + "campaignsTitle" : [ "campaign title 1" ], + "schedulingDate" : [ 2020, 2, 4, 7, 10 ] + } + } + """; + FileUtils.writeContent(SCHEDULING_CAMPAIGN_FILE, old_scheduled_campaign); + + PeriodicScheduledCampaign oldSchedule = create(1L, 11L, "campaign title 1", of(2020, 2, 4, 7, 10)); + PeriodicScheduledCampaign newSchedule = create(2L, List.of(22L, 33L, 44L), List.of("campaign title 2", "campaign title 3", "campaign title 4"), of(2021, 3, 5, 8, 11), "MY_ENV", List.of("FIRST_DATASET", "SECOND_DATASET", "")); + // When + List periodicScheduledCampaigns = sut.getAll(); + //Then + assertThat(periodicScheduledCampaigns).hasSize(1); + assertThat(periodicScheduledCampaigns).contains(oldSchedule); + + //// UPDATE + // When + sut.add(newSchedule); + + // Then + periodicScheduledCampaigns = sut.getAll(); + assertThat(periodicScheduledCampaigns).hasSize(2); + assertThat(periodicScheduledCampaigns).contains(oldSchedule); + assertThat(periodicScheduledCampaigns).contains(newSchedule); + + String expectedScheduledCampaignsAfterUpdate = + """ + { + "1" : { + "id" : "1", + "schedulingDate" : [ 2020, 2, 4, 7, 10 ], + "campaignsId" : [ 11 ], + "campaignsTitle" : [ "campaign title 1" ], + "datasetsId" : [ "" ] + }, + "2" : { + "id" : "2", + "schedulingDate" : [ 2021, 3, 5, 8, 11 ], + "environment" : "MY_ENV", + "campaignsId" : [ 22, 33, 44 ], + "campaignsTitle" : [ "campaign title 2", "campaign title 3", "campaign title 4" ], + "datasetsId" : [ "FIRST_DATASET", "SECOND_DATASET", "" ] + } + } + """; + String actualContent = FileUtils.readContent(SCHEDULING_CAMPAIGN_FILE); + assertThat(actualContent).isEqualToIgnoringWhitespace(expectedScheduledCampaignsAfterUpdate); + } + + private PeriodicScheduledCampaign create(Long id, Long campaignId, String campaignTitle, LocalDateTime nextExecutionDate) { + return create(id, List.of(campaignId), List.of(campaignTitle), nextExecutionDate, null, null); + } + + private PeriodicScheduledCampaign create(Long campaignId, String campaignTitle, LocalDateTime nextExecutionDate) { + return create(null, campaignId, campaignTitle, nextExecutionDate); + } + + private PeriodicScheduledCampaign create(Long campaignId, String campaignTitle, LocalDateTime nextExecutionDate, String environment, String datasetId) { + return create(null, List.of(campaignId), List.of(campaignTitle), nextExecutionDate, environment, List.of(datasetId)); + } + + private PeriodicScheduledCampaign create(Long id, List campaignsId, List campaignsTitle, LocalDateTime nextExecutionDate, String environment, final List datasetIds) { + List campaignExecutionRequests = + IntStream.range(0, campaignsId.size()) + .mapToObj(i -> { + var tmpDatasetIdList = datasetIds; + if(datasetIds == null) { + tmpDatasetIdList = campaignsId.stream().map(c -> "").toList(); + } + return new CampaignExecutionRequest( + campaignsId.get(i), + campaignsTitle.get(i), + tmpDatasetIdList.get(i)); + }) + .toList(); + return new PeriodicScheduledCampaign(id, nextExecutionDate, Frequency.EMPTY, environment, campaignExecutionRequests); + } +} diff --git a/chutney/server/src/test/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngineTest.java b/chutney/server/src/test/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngineTest.java index 1ac2bb384..92bb98455 100644 --- a/chutney/server/src/test/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngineTest.java +++ b/chutney/server/src/test/java/com/chutneytesting/execution/domain/campaign/CampaignExecutionEngineTest.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -63,6 +64,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.springframework.core.task.support.ExecutorServiceAdapter; @@ -602,7 +605,6 @@ public void should_execute_by_id_with_inline_dataset() { CampaignExecution campaignExecution = sut.executeById(campaignId, env, dataSet, "USER"); // Then - assertThat(campaign.executionDataset()).isNull(); assertThat(campaignExecution.dataset).isNotNull(); assertThat(campaignExecution.dataset.id).isNull(); assertThat(campaignExecution.dataset.constants).isEqualTo(constants); @@ -628,7 +630,6 @@ public void should_execute_by_id_with_known_dataset() { CampaignExecution campaignExecution = sut.executeById(campaignId, env, dataSet, "USER"); // Then - assertThat(campaign.executionDataset()).isNull(); assertThat(campaignExecution.dataset).isNotNull(); assertThat(campaignExecution.dataset.constants).isEmpty(); assertThat(campaignExecution.dataset.datatable).isEmpty(); @@ -677,6 +678,32 @@ public void should_execute_by_id_without_any_dataset() { assertThat(campaignExecution.dataset).isNull(); } + @ParameterizedTest + @CsvSource({ + "123, DEV, dataset123, user123", + "123, DEV, , user123" + }) + void testExecuteScheduledCampaign(Long campaignId, String environment, String datasetId, String userId) { + //Given + var scenarios = Lists.list(firstTestCase.id()).stream().map(id -> new Campaign.CampaignScenario(id, null)).toList(); + Campaign campaign = new Campaign(1L, "campaign1", null, scenarios, "DEV", false, false, null, List.of("TAG")); + when(campaignRepository.findById(any())).thenReturn(campaign); + if (datasetId != null) { + when(datasetRepository.findById(datasetId)).thenReturn(DataSet.NO_DATASET); + } + + // When + CampaignExecutionEngine spySut = spy(sut); + spySut.executeScheduledCampaign(campaignId, environment, datasetId, userId); + + // Then + if (datasetId != null) { + verify(spySut, times(1)).executeById(campaignId, environment, DataSet.NO_DATASET, userId); + } else { + verify(spySut, times(1)).executeById(campaignId, environment, null, userId); + } + } + private final static Random campaignIdGenerator = new Random(); private Long generateId() { diff --git a/chutney/server/src/test/java/com/chutneytesting/execution/domain/schedule/CampaignSchedulerTest.java b/chutney/server/src/test/java/com/chutneytesting/execution/domain/schedule/CampaignSchedulerTest.java index ff8d91eb6..85b0768b1 100644 --- a/chutney/server/src/test/java/com/chutneytesting/execution/domain/schedule/CampaignSchedulerTest.java +++ b/chutney/server/src/test/java/com/chutneytesting/execution/domain/schedule/CampaignSchedulerTest.java @@ -12,175 +12,145 @@ import static java.time.LocalDateTime.now; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static java.util.List.of; import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static util.AssertTestUtils.softAssertVerifies; import com.chutneytesting.campaign.domain.Frequency; import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign; -import com.chutneytesting.campaign.domain.PeriodicScheduledCampaignRepository; +import com.chutneytesting.campaign.domain.PeriodicScheduledCampaign.CampaignExecutionRequest; +import com.chutneytesting.campaign.domain.ScheduledCampaignRepository; import com.chutneytesting.execution.domain.campaign.CampaignExecutionEngine; +import com.chutneytesting.server.core.domain.dataset.DataSet; import java.time.Clock; -import java.time.LocalDateTime; import java.util.List; import java.util.Random; import java.util.concurrent.Executors; -import org.junit.jupiter.api.Assertions; +import org.junit.Rule; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.mockito.InOrder; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.VerificationCollector; public class CampaignSchedulerTest { - private CampaignScheduler sut; + @Rule + public final VerificationCollector collector = MockitoJUnit.collector(); + private CampaignScheduler sut; private final CampaignExecutionEngine campaignExecutionEngine = mock(CampaignExecutionEngine.class); - private final PeriodicScheduledCampaignRepository periodicScheduledCampaignRepository = mock(PeriodicScheduledCampaignRepository.class); + private final ScheduledCampaignRepository scheduledCampaignRepository = mock(ScheduledCampaignRepository.class); + private Clock clock; + private String environment; + private DataSet dataset; @BeforeEach public void setUp() { clock = Clock.systemDefaultZone(); - sut = new CampaignScheduler(campaignExecutionEngine, clock, periodicScheduledCampaignRepository, Executors.newFixedThreadPool(2)); + sut = new CampaignScheduler(campaignExecutionEngine, clock, scheduledCampaignRepository, Executors.newFixedThreadPool(2)); + environment = randomAlphanumeric(10); + dataset = DataSet.builder() + .withId(randomAlphanumeric(10)) + .withName(randomAlphanumeric(10)) + .build(); } @ParameterizedTest() @EnumSource(Frequency.class) - void should_execute_campaign_as_internal_user_named_auto_when_executing_periodic_scheduled_campaign(Frequency frequency) { - List periodicScheduledCampaign = createPeriodicScheduledCampaigns(singletonList(frequency)); - when(periodicScheduledCampaignRepository.getAll()) + void schedule_campaign_execution(Frequency frequency) { + List periodicScheduledCampaign = createPeriodicScheduledCampaigns(singletonList(frequency), environment, dataset.id); + when(scheduledCampaignRepository.getAll()) .thenReturn( periodicScheduledCampaign ); sut.executeScheduledCampaigns(); - verify(campaignExecutionEngine).executeById(periodicScheduledCampaign.get(0).campaignsId.get(0), "auto"); - } - - @ParameterizedTest() - @EnumSource(Frequency.class) - void should_remove_last_execution_when_executing_periodic_scheduled_campaign(Frequency frequency) { - List periodicScheduledCampaign = createPeriodicScheduledCampaigns(singletonList(frequency)); - when(periodicScheduledCampaignRepository.getAll()) - .thenReturn( - periodicScheduledCampaign - ); - - sut.executeScheduledCampaigns(); - - verify(periodicScheduledCampaignRepository).removeById(periodicScheduledCampaign.get(0).id); - } - - @ParameterizedTest() - @EnumSource(Frequency.class) - void should_add_next_execution_when_executing_periodic_scheduled_campaign_except_for_EMPTY_frequency(Frequency frequency) { - List periodicScheduledCampaign = createPeriodicScheduledCampaigns(singletonList(frequency)); - when(periodicScheduledCampaignRepository.getAll()) - .thenReturn( - periodicScheduledCampaign - ); - - sut.executeScheduledCampaigns(); - - if (EMPTY.equals(frequency)) { - verify(periodicScheduledCampaignRepository, times(0)).add(any()); - } else { - verify(periodicScheduledCampaignRepository).add( - periodicScheduledCampaign.get(0).nextScheduledExecution() - ); - } - } - - @Test - void should_reschedule_missed_campaign_with_frequency_from_now_in_the_future() { - PeriodicScheduledCampaign scheduledCampaignWeeklyFrequency = new PeriodicScheduledCampaign(1L, List.of(11L), List.of("campaign title 1"), LocalDateTime.now().minusWeeks(5).minusHours(1).plusMinutes(15).withSecond(0).withNano(0), Frequency.WEEKLY); - PeriodicScheduledCampaign scheduledCampaignHourlyFrequency = new PeriodicScheduledCampaign(2L, List.of(22L), List.of("campaign title 2"), LocalDateTime.now().minusHours(6).plusMinutes(15).withSecond(0).withNano(0), Frequency.HOURLY); - PeriodicScheduledCampaign scheduledCampaignNoFrequency = new PeriodicScheduledCampaign(3L, List.of(33L), List.of("campaign title 3"), LocalDateTime.now().minusYears(1).minusHours(6).plusMinutes(15).withSecond(0).withNano(0)); - when(periodicScheduledCampaignRepository.getAll()) - .thenReturn( - List.of(scheduledCampaignWeeklyFrequency, scheduledCampaignHourlyFrequency, scheduledCampaignNoFrequency) - ); - when(periodicScheduledCampaignRepository.add(any())) - .thenReturn( - null - ); - doNothing().when(periodicScheduledCampaignRepository).removeById(any()); - - PeriodicScheduledCampaign expected1 = new PeriodicScheduledCampaign(1L, List.of(11L), List.of("campaign title 1"), LocalDateTime.now().plusWeeks(1).minusHours(1).plusMinutes(15).withSecond(0).withNano(0), Frequency.WEEKLY); - PeriodicScheduledCampaign expected2 = new PeriodicScheduledCampaign(2L, List.of(22L), List.of("campaign title 2"), LocalDateTime.now().plusMinutes(15).withSecond(0).withNano(0), Frequency.HOURLY); - - - // WHEN - sut.scheduledMissedCampaignIds(); - - // THEN - verify(periodicScheduledCampaignRepository).add(expected1); - verify(periodicScheduledCampaignRepository).add(expected2); - verify(periodicScheduledCampaignRepository).removeById(1L); - verify(periodicScheduledCampaignRepository).removeById(2L); - verify(periodicScheduledCampaignRepository).removeById(3L); + softAssertVerifies(of( + // Execution called with scheduledCampaign parameter with user "auto" + () -> verify(campaignExecutionEngine).executeScheduledCampaign(periodicScheduledCampaign.get(0).campaignExecutionRequests.get(0).campaignId(), environment, dataset.id, "auto"), + // Last execution is removed + () -> verify(scheduledCampaignRepository).removeById(periodicScheduledCampaign.get(0).id), + // Add next execution except for EMPTY frequency + () -> { + if (EMPTY.equals(frequency)) { + verify(scheduledCampaignRepository, times(0)).add(any()); + } else { + verify(scheduledCampaignRepository).add(periodicScheduledCampaign.get(0).nextScheduledExecution()); + } + } + )); } @Test - void should_not_explode_when_runtime_exceptions_occur_retrieving_campaigns_to_execute() { - when(periodicScheduledCampaignRepository.getAll()) + void should_not_throw_exception_when_runtime_exceptions_occur_retrieving_campaigns_to_execute() { + when(scheduledCampaignRepository.getAll()) .thenThrow(new RuntimeException("scheduledCampaignRepository.getAll()")); - Assertions.assertDoesNotThrow( + + assertDoesNotThrow( () -> sut.executeScheduledCampaigns() ); - - verify(periodicScheduledCampaignRepository).getAll(); + verify(scheduledCampaignRepository).getAll(); } @Test - void should_not_explode_when_runtime_exceptions_occur_executing_campaigns() { + void should_not_throw_exception_when_runtime_exceptions_occur_executing_campaigns() { List periodicScheduledCampaigns = createPeriodicScheduledCampaigns(asList(Frequency.MONTHLY, Frequency.DAILY)); - when(periodicScheduledCampaignRepository.getAll()) + when(scheduledCampaignRepository.getAll()) .thenReturn( periodicScheduledCampaigns ); - when(campaignExecutionEngine.executeById(periodicScheduledCampaigns.get(0).campaignsId.get(0), SCHEDULER_EXECUTE_USER)) + when(campaignExecutionEngine.executeById(periodicScheduledCampaigns.get(0).campaignExecutionRequests.get(0).campaignId(), SCHEDULER_EXECUTE_USER)) .thenThrow(new RuntimeException("campaignExecutionEngine.executeById")); - Assertions.assertDoesNotThrow( + assertDoesNotThrow( () -> sut.executeScheduledCampaigns() ); - verify(campaignExecutionEngine, times(periodicScheduledCampaigns.size())).executeById(any(), any()); + verify(campaignExecutionEngine, times(periodicScheduledCampaigns.size())).executeScheduledCampaign(any(), any(), any(), any()); + verify(campaignExecutionEngine, times(periodicScheduledCampaigns.size())).executeScheduledCampaign(any(), any(), any(), any()); } @Test - void should_execute_sequentially_when_executing_periodic_scheduled_campaigns() { - PeriodicScheduledCampaign sc1 = new PeriodicScheduledCampaign(1L, List.of(11L, 22L), List.of("cpg 11", "cpg 22"), now(clock).minusSeconds(5), Frequency.HOURLY); + void should_execute_sequentially_campaigns() { + PeriodicScheduledCampaign sc1 = new PeriodicScheduledCampaign(1L, now(clock).minusSeconds(5), Frequency.HOURLY, environment, of(new CampaignExecutionRequest(11L, "campaign title 1", dataset.id), new CampaignExecutionRequest(22L, "campaign title 2", dataset.id))); - List periodicScheduledCampaign = List.of(sc1); - when(periodicScheduledCampaignRepository.getAll()) + List periodicScheduledCampaigns = of(sc1); + when(scheduledCampaignRepository.getAll()) .thenReturn( - periodicScheduledCampaign + periodicScheduledCampaigns ); InOrder inOrder = inOrder(campaignExecutionEngine); sut.executeScheduledCampaigns(); - inOrder.verify(campaignExecutionEngine).executeById(eq(periodicScheduledCampaign.get(0).campaignsId.get(0)), eq("auto")); - inOrder.verify(campaignExecutionEngine).executeById(eq(periodicScheduledCampaign.get(0).campaignsId.get(1)), eq("auto")); - verify(campaignExecutionEngine, times(2)).executeById(any(), any()); + inOrder.verify(campaignExecutionEngine).executeScheduledCampaign(eq(periodicScheduledCampaigns.get(0).campaignExecutionRequests.get(0).campaignId()), eq(environment), eq(dataset.id), eq("auto")); + inOrder.verify(campaignExecutionEngine).executeScheduledCampaign(eq(periodicScheduledCampaigns.get(0).campaignExecutionRequests.get(1).campaignId()), eq(environment), eq(dataset.id), eq("auto")); + verify(campaignExecutionEngine, times(2)).executeScheduledCampaign(any(), any(), any(), any()); } private List createPeriodicScheduledCampaigns(List frequencies) { + return createPeriodicScheduledCampaigns(frequencies, null, null); + } + + private List createPeriodicScheduledCampaigns(List frequencies, String environment, String datasetId) { Random rand = new Random(); return frequencies.stream() .map(f -> - new PeriodicScheduledCampaign(rand.nextLong(), List.of(rand.nextLong()), List.of("title"), now(clock).minusSeconds(5), f) + new PeriodicScheduledCampaign(rand.nextLong(), now(clock).minusSeconds(5), f, environment, of(new CampaignExecutionRequest( rand.nextLong(), "title", datasetId))) ) .collect(toList()); } diff --git a/chutney/server/src/test/java/util/AssertTestUtils.java b/chutney/server/src/test/java/util/AssertTestUtils.java new file mode 100644 index 000000000..a62581e2c --- /dev/null +++ b/chutney/server/src/test/java/util/AssertTestUtils.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 Enedis + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package util; + +import java.util.List; +import org.assertj.core.api.SoftAssertions; + +public class AssertTestUtils { + + public static void softAssertVerifies(List verifies) { + SoftAssertions softly = new SoftAssertions(); + for (Runnable verify : verifies) { + softly.assertThatCode(verify::run).doesNotThrowAnyException(); + } + softly.assertAll(); + } +} diff --git a/chutney/ui/src/app/core/model/campaign/campaign-scheduling.model.ts b/chutney/ui/src/app/core/model/campaign/campaign-scheduling.model.ts index 72eb1d0f9..9994cc7f6 100644 --- a/chutney/ui/src/app/core/model/campaign/campaign-scheduling.model.ts +++ b/chutney/ui/src/app/core/model/campaign/campaign-scheduling.model.ts @@ -7,15 +7,17 @@ import { FREQUENCY } from '@core/model/campaign/FREQUENCY'; +export interface CampaignExecutionRequest { + campaignId: number; + campaignTitle: string; + datasetId: string; +} -export class CampaignScheduling { - - constructor( - public campaignsId: number[], - public campaignsTitle: string[], - public schedulingDate: Date, - public frequency?: FREQUENCY, - public id?: number - ) { - } +export interface CampaignScheduling { + id?: number; + schedulingDate: Date; + frequency: FREQUENCY; + environment: string; + campaignExecutionRequest: CampaignExecutionRequest[]; } + diff --git a/chutney/ui/src/app/core/services/campaign-scheduling.service.ts b/chutney/ui/src/app/core/services/campaign-scheduling.service.ts index 04c1da682..bdb761f2e 100644 --- a/chutney/ui/src/app/core/services/campaign-scheduling.service.ts +++ b/chutney/ui/src/app/core/services/campaign-scheduling.service.ts @@ -24,7 +24,6 @@ export class CampaignSchedulingService { findAll(): Observable> { return this.http.get>(environment.backend + this.resourceUrl).pipe(map((res: Array) => { - //res.sort((a, b) => a.executionScheduled < b.executionScheduled)); return res; })); } @@ -36,6 +35,4 @@ export class CampaignSchedulingService { delete(id: number): Observable { return this.http.delete(environment.backend + `${this.resourceUrl}/${id}`); } - - } diff --git a/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.html b/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.html index cf3458a79..bbe9cc258 100644 --- a/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.html +++ b/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.html @@ -133,7 +133,7 @@

{{ 'campaigns.list.executions.next' | translate }}

@for (scheduledCampaign of scheduledCampaigns; track scheduledCampaign.id) { - #{{scheduledCampaign.campaignId}} + #{{scheduledCampaign.id}} @if (isFrequencyCampaign(scheduledCampaign)) { {{ 'campaigns.list.executions.next' | translate }} }" aria-hidden="true"> } - - {{scheduledCampaign.campaignsTitle}} + - {{ getCampaignTitles(scheduledCampaign) }} {{scheduledCampaign.schedulingDate | amLocal | amDateFormat: 'YYYY-MM-DD HH:mm'}} diff --git a/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.ts b/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.ts index 5df402481..d52d4eb6e 100644 --- a/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.ts +++ b/chutney/ui/src/app/modules/campaign/components/campaign-list/campaign-list.component.ts @@ -89,7 +89,7 @@ export class CampaignListComponent implements OnInit, OnDestroy { } scheduledCampaignHasMissingEnv(scheduledCampaign: CampaignScheduling): boolean { - return scheduledCampaign.campaignsId.some(campaignId => { + return scheduledCampaign.campaignExecutionRequest.map(cer => cer.campaignId).some(campaignId => { const campaign = this.campaigns.find(campaign => campaign.id === campaignId); return campaign == null || campaign.environment == null; }); @@ -286,4 +286,9 @@ export class CampaignListComponent implements OnInit, OnDestroy { toIsoDate(date: string) { return new Date(date); } + + getCampaignTitles(scheduledCampaign: CampaignScheduling) { + const titles = scheduledCampaign.campaignExecutionRequest.map(req => req.campaignTitle); + return titles.join(', '); + } } diff --git a/chutney/ui/src/app/modules/campaign/components/campaign-scheduling/campaign-scheduling.component.html b/chutney/ui/src/app/modules/campaign/components/campaign-scheduling/campaign-scheduling.component.html index 237c3563b..c49344b60 100644 --- a/chutney/ui/src/app/modules/campaign/components/campaign-scheduling/campaign-scheduling.component.html +++ b/chutney/ui/src/app/modules/campaign/components/campaign-scheduling/campaign-scheduling.component.html @@ -5,7 +5,7 @@ ~ --> -
+
@@ -24,73 +24,116 @@

{{ 'campaigns.scheduling.title' | translate }}

} -
-
+ +
- -
- - -
+
+
+ + +
+ + +
- @if (submitted && f['date'].errors) { -
- {{ 'campaigns.scheduling.required.date' | translate }} + @if (submitted && f['date'].errors) { +
+ {{ 'campaigns.scheduling.required.date' | translate }} +
+ }
- } -
-
- - - @if (submitted && f['time'].errors) { -
- {{ 'campaigns.scheduling.required.time' | translate }} +
+ + + + @if (submitted && f['time'].errors) { +
+ {{ 'campaigns.scheduling.required.time' | translate }} +
+ } +
+
+ + + +
+
+ + +
- } -
-
- - -
-
- @if (campaigns.length > 0) { -
-
- - - @if (submitted && f.selectedCampaigns.errors?.required) { -
- {{ 'campaigns.scheduling.required.campaign' | translate }} -
- }
+ @if (campaigns.length > 0) { +
-
    - @for (campaign of form.get('selectedCampaigns').value; track campaign.id; let i = $index) { -
  • - {{ campaign.title }} - -
  • - } -
+
+
+ {{ 'campaigns.scheduling.campaign' | translate }} + + + @if (submitted && f.selectedCampaigns.errors?.required) { +
+ {{ 'campaigns.scheduling.required.campaign' | translate }} +
+ } +
+
+ @if (form.get('selectedCampaigns').value != 0) { +
+ + + + + + + + + @for (campaign of form.get('selectedCampaigns').value; track campaign.id; let i = $index) { + + + + + + + } +
#{{ 'campaigns.scheduling.campaign' | translate }}{{ 'global.words.dataset' | translate }}
#{{ campaign.id }}{{ campaign.title }} + + +
+
+ }
- } - -
+