diff --git a/monitor-commons/src/main/java/org/gridsuite/monitor/commons/ProcessRunMessage.java b/monitor-commons/src/main/java/org/gridsuite/monitor/commons/ProcessRunMessage.java index 95d5c71..8783488 100644 --- a/monitor-commons/src/main/java/org/gridsuite/monitor/commons/ProcessRunMessage.java +++ b/monitor-commons/src/main/java/org/gridsuite/monitor/commons/ProcessRunMessage.java @@ -21,7 +21,8 @@ public record ProcessRunMessage( @JsonSubTypes({ @JsonSubTypes.Type(value = SecurityAnalysisConfig.class, name = "SECURITY_ANALYSIS") }) - T config + T config, + boolean isDebug ) { public ProcessType processType() { return config.processType(); diff --git a/monitor-commons/src/main/java/org/gridsuite/monitor/commons/utils/S3PathUtils.java b/monitor-commons/src/main/java/org/gridsuite/monitor/commons/utils/S3PathUtils.java new file mode 100644 index 0000000..fa819a5 --- /dev/null +++ b/monitor-commons/src/main/java/org/gridsuite/monitor/commons/utils/S3PathUtils.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.monitor.commons.utils; + +import java.util.UUID; + +/** + * @author Kevin Le Saulnier + */ +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +public class S3PathUtils { + public static final String S3_DELIMITER = "/"; + + /** + * Builds root path used to build debug file location + * @param executionEnvName + * @param processType + * @param executionId + * @return {executionEnvName}_debug/process/{processType}/{executionId} + */ + public static String toDebugLocation(String executionEnvName, String processType, UUID executionId) { + return String.join(S3_DELIMITER, executionEnvName + "_debug", "process", processType, executionId.toString()); + } +} diff --git a/monitor-server/src/main/java/org/gridsuite/monitor/server/controllers/MonitorController.java b/monitor-server/src/main/java/org/gridsuite/monitor/server/controllers/MonitorController.java index 65ec8bc..bb56fdd 100644 --- a/monitor-server/src/main/java/org/gridsuite/monitor/server/controllers/MonitorController.java +++ b/monitor-server/src/main/java/org/gridsuite/monitor/server/controllers/MonitorController.java @@ -44,9 +44,10 @@ public MonitorController(MonitorService monitorService) { @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis execution has been started")}) public ResponseEntity executeSecurityAnalysis( @RequestParam UUID caseUuid, + @RequestParam(required = false, defaultValue = "false") boolean isDebug, @RequestBody SecurityAnalysisConfig securityAnalysisConfig, @RequestHeader(HEADER_USER_ID) String userId) { - UUID executionId = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig); + UUID executionId = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig, isDebug); return ResponseEntity.ok(executionId); } diff --git a/monitor-server/src/main/java/org/gridsuite/monitor/server/entities/ProcessExecutionEntity.java b/monitor-server/src/main/java/org/gridsuite/monitor/server/entities/ProcessExecutionEntity.java index eaaeaa1..7ef13f4 100644 --- a/monitor-server/src/main/java/org/gridsuite/monitor/server/entities/ProcessExecutionEntity.java +++ b/monitor-server/src/main/java/org/gridsuite/monitor/server/entities/ProcessExecutionEntity.java @@ -55,6 +55,12 @@ public class ProcessExecutionEntity { @Column private String userId; + @Column + private boolean isDebug; + + @Column + private String debugFileLocation; + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) @JoinColumn(name = "execution_id", foreignKey = @ForeignKey(name = "processExecutionStep_processExecution_fk")) @OrderBy("stepOrder ASC") diff --git a/monitor-server/src/main/java/org/gridsuite/monitor/server/services/MonitorService.java b/monitor-server/src/main/java/org/gridsuite/monitor/server/services/MonitorService.java index d0722e5..89a34b5 100644 --- a/monitor-server/src/main/java/org/gridsuite/monitor/server/services/MonitorService.java +++ b/monitor-server/src/main/java/org/gridsuite/monitor/server/services/MonitorService.java @@ -11,6 +11,7 @@ import org.gridsuite.monitor.commons.ProcessStatus; import org.gridsuite.monitor.commons.ProcessType; import org.gridsuite.monitor.commons.ResultInfos; +import org.gridsuite.monitor.commons.utils.S3PathUtils; import org.gridsuite.monitor.server.dto.ProcessExecution; import org.gridsuite.monitor.server.dto.ReportPage; import org.gridsuite.monitor.server.entities.ProcessExecutionEntity; @@ -49,17 +50,18 @@ public MonitorService(ProcessExecutionRepository executionRepository, } @Transactional - public UUID executeProcess(UUID caseUuid, String userId, ProcessConfig processConfig) { + public UUID executeProcess(UUID caseUuid, String userId, ProcessConfig processConfig, boolean isDebug) { ProcessExecutionEntity execution = ProcessExecutionEntity.builder() .type(processConfig.processType().name()) .caseUuid(caseUuid) .status(ProcessStatus.SCHEDULED) .scheduledAt(Instant.now()) .userId(userId) + .isDebug(isDebug) .build(); executionRepository.save(execution); - notificationService.sendProcessRunMessage(caseUuid, processConfig, execution.getId()); + notificationService.sendProcessRunMessage(caseUuid, processConfig, isDebug, execution.getId()); return execution.getId(); } @@ -77,6 +79,10 @@ public void updateExecutionStatus(UUID executionId, ProcessStatus status, String if (completedAt != null) { execution.setCompletedAt(completedAt); } + if (execution.isDebug() && executionEnvName != null + && (status.equals(ProcessStatus.COMPLETED) || status.equals(ProcessStatus.FAILED))) { + execution.setDebugFileLocation(S3PathUtils.toDebugLocation(executionEnvName, execution.getType(), executionId)); + } executionRepository.save(execution); }); } diff --git a/monitor-server/src/main/java/org/gridsuite/monitor/server/services/NotificationService.java b/monitor-server/src/main/java/org/gridsuite/monitor/server/services/NotificationService.java index a6ca955..d7ce14d 100644 --- a/monitor-server/src/main/java/org/gridsuite/monitor/server/services/NotificationService.java +++ b/monitor-server/src/main/java/org/gridsuite/monitor/server/services/NotificationService.java @@ -23,11 +23,11 @@ public class NotificationService { private final StreamBridge publisher; - public void sendProcessRunMessage(UUID caseUuid, ProcessConfig processConfig, UUID executionId) { + public void sendProcessRunMessage(UUID caseUuid, ProcessConfig processConfig, boolean isDebug, UUID executionId) { String bindingName = switch (processConfig.processType()) { case SECURITY_ANALYSIS -> "publishRunSecurityAnalysis-out-0"; }; - ProcessRunMessage message = new ProcessRunMessage<>(executionId, caseUuid, processConfig); + ProcessRunMessage message = new ProcessRunMessage<>(executionId, caseUuid, processConfig, isDebug); publisher.send(bindingName, message); } } diff --git a/monitor-server/src/main/resources/db/changelog/changesets/changelog_20260212T082157Z.xml b/monitor-server/src/main/resources/db/changelog/changesets/changelog_20260212T082157Z.xml new file mode 100644 index 0000000..5559b76 --- /dev/null +++ b/monitor-server/src/main/resources/db/changelog/changesets/changelog_20260212T082157Z.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/monitor-server/src/main/resources/db/changelog/db.changelog-master.yaml b/monitor-server/src/main/resources/db/changelog/db.changelog-master.yaml index 44dd608..e8f62f7 100644 --- a/monitor-server/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/monitor-server/src/main/resources/db/changelog/db.changelog-master.yaml @@ -8,4 +8,6 @@ databaseChangeLog: - include: file: changesets/changelog_20260130T160426Z.xml relativeToChangelogFile: true - + - include: + file: changesets/changelog_20260212T082157Z.xml + relativeToChangelogFile: true diff --git a/monitor-server/src/test/java/org/gridsuite/monitor/server/MonitorIntegrationTest.java b/monitor-server/src/test/java/org/gridsuite/monitor/server/MonitorIntegrationTest.java index 3111c7c..8760e27 100644 --- a/monitor-server/src/test/java/org/gridsuite/monitor/server/MonitorIntegrationTest.java +++ b/monitor-server/src/test/java/org/gridsuite/monitor/server/MonitorIntegrationTest.java @@ -102,7 +102,7 @@ void securityAnalysisProcessIT() throws Exception { UUID.randomUUID(), List.of("contingency1", "contingency2"), List.of(UUID.randomUUID())); - UUID executionId = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig); + UUID executionId = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig, false); // Verify message was published Message sentMessage = outputDestination.receive(1000, PROCESS_SA_RUN_DESTINATION); diff --git a/monitor-server/src/test/java/org/gridsuite/monitor/server/controllers/MonitorControllerTest.java b/monitor-server/src/test/java/org/gridsuite/monitor/server/controllers/MonitorControllerTest.java index 18c6a10..6b6bbe3 100644 --- a/monitor-server/src/test/java/org/gridsuite/monitor/server/controllers/MonitorControllerTest.java +++ b/monitor-server/src/test/java/org/gridsuite/monitor/server/controllers/MonitorControllerTest.java @@ -18,11 +18,15 @@ import org.gridsuite.monitor.server.dto.Severity; import org.gridsuite.monitor.server.services.MonitorService; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import java.time.Instant; import java.util.List; @@ -54,8 +58,10 @@ class MonitorControllerTest { @MockitoBean private MonitorService monitorService; - @Test - void executeSecurityAnalysisShouldReturnExecutionId() throws Exception { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @NullSource + void executeSecurityAnalysisShouldReturnExecutionId(Boolean isDebug) throws Exception { UUID caseUuid = UUID.randomUUID(); UUID parametersUuid = UUID.randomUUID(); UUID modificationUuid = UUID.randomUUID(); @@ -65,20 +71,27 @@ void executeSecurityAnalysisShouldReturnExecutionId() throws Exception { List.of("contingency1", "contingency2"), List.of(modificationUuid) ); + boolean expectedDebugValue = Boolean.TRUE.equals(isDebug); - when(monitorService.executeProcess(any(UUID.class), any(String.class), any(SecurityAnalysisConfig.class))) + when(monitorService.executeProcess(any(UUID.class), any(String.class), any(SecurityAnalysisConfig.class), eq(expectedDebugValue))) .thenReturn(executionId); - mockMvc.perform(post("/v1/execute/security-analysis") - .param("caseUuid", caseUuid.toString()) - .header("userId", "user1") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(config))) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$").value(executionId.toString())); + MockHttpServletRequestBuilder request = post("/v1/execute/security-analysis") + .param("caseUuid", caseUuid.toString()) + .header("userId", "user1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(config)); + + if (isDebug != null) { + request.param("isDebug", isDebug.toString()); + } + + mockMvc.perform(request) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").value(executionId.toString())); - verify(monitorService).executeProcess(eq(caseUuid), any(String.class), any(SecurityAnalysisConfig.class)); + verify(monitorService).executeProcess(eq(caseUuid), any(String.class), any(SecurityAnalysisConfig.class), eq(expectedDebugValue)); } @Test diff --git a/monitor-server/src/test/java/org/gridsuite/monitor/server/services/MonitorServiceTest.java b/monitor-server/src/test/java/org/gridsuite/monitor/server/services/MonitorServiceTest.java index fd1c712..baddb4c 100644 --- a/monitor-server/src/test/java/org/gridsuite/monitor/server/services/MonitorServiceTest.java +++ b/monitor-server/src/test/java/org/gridsuite/monitor/server/services/MonitorServiceTest.java @@ -7,6 +7,7 @@ package org.gridsuite.monitor.server.services; import org.gridsuite.monitor.commons.*; +import org.gridsuite.monitor.commons.utils.S3PathUtils; import org.gridsuite.monitor.server.dto.ProcessExecution; import org.gridsuite.monitor.server.dto.ReportLog; import org.gridsuite.monitor.server.dto.ReportPage; @@ -17,6 +18,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -69,8 +73,9 @@ void setUp() { ); } - @Test - void executeProcessCreateExecutionAndSendNotification() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void executeProcessCreateExecutionAndSendNotification(boolean isDebug) { UUID expectedExecutionId = UUID.randomUUID(); when(executionRepository.save(any(ProcessExecutionEntity.class))) .thenAnswer(invocation -> { @@ -79,7 +84,7 @@ void executeProcessCreateExecutionAndSendNotification() { return entity; }); - UUID result = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig); + UUID result = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig, isDebug); assertThat(result).isNotNull(); verify(executionRepository).save(argThat(execution -> @@ -94,6 +99,7 @@ void executeProcessCreateExecutionAndSendNotification() { verify(notificationService).sendProcessRunMessage( caseUuid, securityAnalysisConfig, + isDebug, result ); } @@ -145,6 +151,62 @@ void updateExecutionStatusShouldUpdateAllFields() { verify(executionRepository).save(execution); } + @ParameterizedTest + @EnumSource(value = ProcessStatus.class, names = {"COMPLETED", "FAILED"}) + void updateExecutionStatusWithDebugOnAndProcessStatusDoneShouldUpdateDebugLocation(ProcessStatus processStatus) { + ProcessExecutionEntity execution = ProcessExecutionEntity.builder() + .id(executionId) + .type(ProcessType.SECURITY_ANALYSIS.name()) + .caseUuid(caseUuid) + .userId(userId) + .status(ProcessStatus.RUNNING) + .scheduledAt(Instant.now()) + .isDebug(true) + .build(); + when(executionRepository.findById(executionId)).thenReturn(Optional.of(execution)); + String envName = "production-env"; + Instant startedAt = Instant.now().minusSeconds(60); + Instant completedAt = Instant.now(); + + monitorService.updateExecutionStatus(executionId, processStatus, envName, startedAt, completedAt); + + verify(executionRepository).findById(executionId); + // assert standard fields + assertThat(execution.getStatus()).isEqualTo(processStatus); + assertThat(execution.getExecutionEnvName()).isEqualTo(envName); + assertThat(execution.getStartedAt()).isEqualTo(startedAt); + assertThat(execution.getCompletedAt()).isEqualTo(completedAt); + + // assert debug location field + assertThat(execution.getDebugFileLocation()).isEqualTo(S3PathUtils.toDebugLocation(envName, ProcessType.SECURITY_ANALYSIS.name(), executionId)); + verify(executionRepository).save(execution); + } + + @ParameterizedTest + @EnumSource(value = ProcessStatus.class, names = {"RUNNING", "SCHEDULED"}) + void updateExecutionStatusWithDebugOnAndProcessStatusNotDoneShouldNotUpdateDebugLocation(ProcessStatus processStatus) { + ProcessExecutionEntity execution = ProcessExecutionEntity.builder() + .id(executionId) + .type(ProcessType.SECURITY_ANALYSIS.name()) + .caseUuid(caseUuid) + .userId(userId) + .status(ProcessStatus.RUNNING) + .scheduledAt(Instant.now()) + .isDebug(true) + .build(); + when(executionRepository.findById(executionId)).thenReturn(Optional.of(execution)); + String envName = "production-env"; + Instant startedAt = Instant.now().minusSeconds(60); + Instant completedAt = Instant.now(); + + monitorService.updateExecutionStatus(executionId, processStatus, envName, startedAt, completedAt); + + verify(executionRepository).findById(executionId); + // assert debug location field + assertThat(execution.getDebugFileLocation()).isNull(); + verify(executionRepository).save(execution); + } + @Test void updateExecutionStatusShouldHandleExecutionNotFound() { when(executionRepository.findById(executionId)).thenReturn(Optional.empty()); diff --git a/monitor-server/src/test/java/org/gridsuite/monitor/server/services/NotificationServiceTest.java b/monitor-server/src/test/java/org/gridsuite/monitor/server/services/NotificationServiceTest.java index daed261..0f3af21 100644 --- a/monitor-server/src/test/java/org/gridsuite/monitor/server/services/NotificationServiceTest.java +++ b/monitor-server/src/test/java/org/gridsuite/monitor/server/services/NotificationServiceTest.java @@ -9,8 +9,9 @@ import org.gridsuite.monitor.commons.ProcessRunMessage; import org.gridsuite.monitor.commons.SecurityAnalysisConfig; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -53,16 +54,18 @@ void setUp() { ); } - @Test - void sendProcessRunMessage() { - notificationService.sendProcessRunMessage(caseUuid, securityAnalysisConfig, executionId); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sendProcessRunMessage(boolean isDebug) { + notificationService.sendProcessRunMessage(caseUuid, securityAnalysisConfig, isDebug, executionId); verify(publisher).send( eq("publishRunSecurityAnalysis-out-0"), argThat((ProcessRunMessage message) -> message.executionId().equals(executionId) && message.caseUuid().equals(caseUuid) && - message.config().equals(securityAnalysisConfig)) + message.config().equals(securityAnalysisConfig) && + message.isDebug() == isDebug) ); } } diff --git a/monitor-worker-server/pom.xml b/monitor-worker-server/pom.xml index 512af36..659926c 100644 --- a/monitor-worker-server/pom.xml +++ b/monitor-worker-server/pom.xml @@ -14,6 +14,7 @@ 0.61.0 + 3.3.1 @@ -108,6 +109,11 @@ com.powsybl powsybl-case-datasource-client + + io.awspring.cloud + spring-cloud-aws-starter-s3 + ${spring-cloud-aws-starter-s3} + diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/config/S3Configuration.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/config/S3Configuration.java new file mode 100644 index 0000000..a72d056 --- /dev/null +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/config/S3Configuration.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.monitor.worker.server.config; + +import org.gridsuite.monitor.worker.server.services.S3RestService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.services.s3.S3Client; + +/** + * @author Kevin Le Saulnier + */ +@Configuration +public class S3Configuration { + private static final Logger LOGGER = LoggerFactory.getLogger(S3Configuration.class); + @Value("${spring.cloud.aws.bucket:ws-bucket}") + private String bucketName; + + @SuppressWarnings("checkstyle:MethodName") + @Bean + public S3RestService S3Service(S3Client s3Client) { + LOGGER.info("Configuring S3Service with bucket: {}", bucketName); + return new S3RestService(s3Client, bucketName); + } +} diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/AbstractProcessStep.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/AbstractProcessStep.java index 2671026..ad6b21f 100644 --- a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/AbstractProcessStep.java +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/AbstractProcessStep.java @@ -8,6 +8,7 @@ import lombok.Getter; import org.gridsuite.monitor.commons.ProcessConfig; +import org.gridsuite.monitor.commons.utils.S3PathUtils; import java.util.UUID; @@ -24,4 +25,11 @@ protected AbstractProcessStep(ProcessStepType type) { this.type = type; this.id = UUID.randomUUID(); } + + protected String getDebugFilePath(ProcessStepExecutionContext context, String fileName) { + return String.join(S3PathUtils.S3_DELIMITER, + S3PathUtils.toDebugLocation(context.getExecutionEnvironment(), context.getConfig().processType().name(), context.getProcessExecutionId()), + context.getProcessStepType().getName() + "_" + context.getStepOrder(), + fileName); + } } diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContext.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContext.java index e4c64f3..04a17cd 100644 --- a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContext.java +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContext.java @@ -22,14 +22,16 @@ public class ProcessExecutionContext { private final UUID executionId; private final UUID caseUuid; private final C config; + private final boolean isDebug; @Setter private Network network; private final String executionEnvName; - public ProcessExecutionContext(UUID executionId, UUID caseUuid, C config, String executionEnvName) { + public ProcessExecutionContext(UUID executionId, UUID caseUuid, C config, boolean isDebug, String executionEnvName) { this.executionId = executionId; this.caseUuid = caseUuid; this.config = config; + this.isDebug = isDebug; this.executionEnvName = executionEnvName; } diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessStepExecutionContext.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessStepExecutionContext.java index 29e4481..eac1a8e 100644 --- a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessStepExecutionContext.java +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/core/ProcessStepExecutionContext.java @@ -67,6 +67,14 @@ public Network getNetwork() { return processContext.getNetwork(); } + public String getExecutionEnvironment() { + return processContext.getExecutionEnvName(); + } + + public boolean isDebug() { + return processContext.isDebug(); + } + public void setNetwork(Network network) { processContext.setNetwork(network); } diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/dto/S3InputStreamInfos.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/dto/S3InputStreamInfos.java new file mode 100644 index 0000000..d6a203d --- /dev/null +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/dto/S3InputStreamInfos.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.monitor.worker.server.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.io.InputStream; + +/** + * @author Kevin Le Saulnier + */ +@Builder +@Getter +public class S3InputStreamInfos { + InputStream inputStream; + String fileName; + Long fileLength; +} diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStep.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStep.java index 8c09d9a..8957709 100644 --- a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStep.java +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStep.java @@ -6,6 +6,7 @@ */ package org.gridsuite.monitor.worker.server.processes.commons.steps; +import com.powsybl.commons.PowsyblException; import com.powsybl.commons.report.ReportNode; import com.powsybl.iidm.network.Network; import org.apache.commons.collections4.CollectionUtils; @@ -13,30 +14,37 @@ import org.gridsuite.monitor.commons.ProcessConfig; import org.gridsuite.monitor.worker.server.core.AbstractProcessStep; import org.gridsuite.monitor.worker.server.core.ProcessStepExecutionContext; -import org.gridsuite.monitor.worker.server.services.FilterService; -import org.gridsuite.monitor.worker.server.services.NetworkModificationRestService; -import org.gridsuite.monitor.worker.server.services.NetworkModificationService; +import org.gridsuite.monitor.worker.server.services.*; import org.springframework.stereotype.Component; +import java.io.IOException; import java.util.List; import java.util.UUID; /** - * @author Antoine Bouhours + * Apply modifications passed in context to network passed in context
+ * If debug is enabled, resulting network will be saved into S3 + * + * @author Antoine Bouhours */ @Component public class ApplyModificationsStep extends AbstractProcessStep { private final NetworkModificationService networkModificationService; private final NetworkModificationRestService networkModificationRestService; - + private final S3Service s3Service; private final FilterService filterService; - public ApplyModificationsStep(NetworkModificationService networkModificationService, NetworkModificationRestService networkModificationRestService, + private static final String DEBUG_FILENAME = "debug.xiidm.gz"; + + public ApplyModificationsStep(NetworkModificationService networkModificationService, + NetworkModificationRestService networkModificationRestService, + S3Service s3Service, FilterService filterService) { super(CommonStepType.APPLY_MODIFICATIONS); this.networkModificationService = networkModificationService; this.networkModificationRestService = networkModificationRestService; + this.s3Service = s3Service; this.filterService = filterService; } @@ -47,6 +55,21 @@ public void execute(ProcessStepExecutionContext context) { if (CollectionUtils.isNotEmpty(modificationIds)) { applyModifications(modificationIds, network, context.getReportInfos().reportNode()); } + if (context.isDebug()) { + try { + exportUpdatedNetworkToS3(context); + } catch (IOException e) { + throw new PowsyblException("An error occurred while saving debug file", e); + } + } + } + + private void exportUpdatedNetworkToS3(ProcessStepExecutionContext context) throws IOException { + s3Service.exportCompressedToS3( + getDebugFilePath(context, DEBUG_FILENAME), + DEBUG_FILENAME, + networkFile -> context.getNetwork().write("XIIDM", null, networkFile) + ); } private void applyModifications(List modificationIds, Network network, ReportNode reportNode) { diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionService.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionService.java index 38b8c78..d1668d4 100644 --- a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionService.java +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionService.java @@ -56,6 +56,7 @@ public void executeProcess(ProcessRunMessage runMes runMessage.executionId(), runMessage.caseUuid(), runMessage.config(), + runMessage.isDebug(), executionEnvName ); diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/S3RestService.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/S3RestService.java new file mode 100644 index 0000000..90a99b2 --- /dev/null +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/S3RestService.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.monitor.worker.server.services; + +import org.gridsuite.monitor.worker.server.dto.S3InputStreamInfos; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; + +/** + * @author Kevin Le Saulnier + */ +public class S3RestService { + public static final String METADATA_FILE_NAME = "file-name"; + + private final S3Client s3Client; + + private final String bucketName; + + public S3RestService(S3Client s3Client, String bucketName) { + this.s3Client = s3Client; + this.bucketName = bucketName; + } + + public void uploadFile(Path filePath, String s3Key, String fileName) throws IOException { + try { + PutObjectRequest putRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .metadata(Map.of(METADATA_FILE_NAME, fileName)) + .build(); + s3Client.putObject(putRequest, RequestBody.fromFile(filePath)); + } catch (SdkException e) { + throw new IOException("Error occurred while uploading file to S3: " + e.getMessage()); + } + } + + public S3InputStreamInfos downloadFile(String s3Key) throws IOException { + try { + GetObjectRequest getRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(); + ResponseInputStream inputStream = s3Client.getObject(getRequest); + return S3InputStreamInfos.builder() + .inputStream(inputStream) + .fileName(inputStream.response().metadata().get(METADATA_FILE_NAME)) + .fileLength(inputStream.response().contentLength()) + .build(); + } catch (SdkException e) { + throw new IOException("Error occurred while downloading file from S3: " + e.getMessage()); + } + } +} diff --git a/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/S3Service.java b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/S3Service.java new file mode 100644 index 0000000..8066737 --- /dev/null +++ b/monitor-worker-server/src/main/java/org/gridsuite/monitor/worker/server/services/S3Service.java @@ -0,0 +1,58 @@ +package org.gridsuite.monitor.worker.server.services; + +import org.springframework.stereotype.Service; +import org.springframework.util.function.ThrowingConsumer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; +import java.util.zip.GZIPOutputStream; + +@Service +public class S3Service { + private final S3RestService s3RestService; + + public S3Service(S3RestService s3RestService) { + this.s3RestService = s3RestService; + } + + public void exportCompressedToS3(String s3Key, String fileName, ThrowingConsumer writer) throws IOException { + FileAttribute> attrs = + PosixFilePermissions.asFileAttribute( + PosixFilePermissions.fromString("rwx------")); + + Path tempDir = Files.createTempDirectory("process-debug", attrs); + Path debugFile = null; + Path compressedDebugFile = null; + + try { + debugFile = Files.createTempFile(tempDir, fileName, ".tmp"); + compressedDebugFile = Files.createTempFile(tempDir, fileName, ".gz"); + + writer.accept(debugFile); + + try (InputStream in = Files.newInputStream(debugFile); + OutputStream out = new GZIPOutputStream(Files.newOutputStream(compressedDebugFile))) { + in.transferTo(out); + } + + s3RestService.uploadFile(compressedDebugFile, s3Key, fileName); + } finally { + if (debugFile != null) { + Files.deleteIfExists(debugFile); + } + if (compressedDebugFile != null) { + Files.deleteIfExists(compressedDebugFile); + } + if (tempDir != null) { + Files.deleteIfExists(tempDir); + } + } + } +} diff --git a/monitor-worker-server/src/main/resources/application-local.yaml b/monitor-worker-server/src/main/resources/application-local.yaml index 54f57ba..1f0fcee 100644 --- a/monitor-worker-server/src/main/resources/application-local.yaml +++ b/monitor-worker-server/src/main/resources/application-local.yaml @@ -4,6 +4,10 @@ server: spring: rabbitmq: addresses: localhost + cloud: + aws: + s3: + endpoint: http://localhost:19000 powsybl: services: diff --git a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContextTest.java b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContextTest.java index 0bc3782..aca34c6 100644 --- a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContextTest.java +++ b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/core/ProcessExecutionContextTest.java @@ -37,7 +37,7 @@ void shouldInitializeCorrectly() { UUID caseUuid = UUID.randomUUID(); String envName = "test-env"; - ProcessExecutionContext processContext = new ProcessExecutionContext<>(executionId, caseUuid, config, envName); + ProcessExecutionContext processContext = new ProcessExecutionContext<>(executionId, caseUuid, config, false, envName); assertThat(processContext.getConfig()).isEqualTo(config); assertThat(processContext.getExecutionId()).isEqualTo(executionId); @@ -48,7 +48,7 @@ void shouldInitializeCorrectly() { @Test void shouldSetAndGetNetwork() { - ProcessExecutionContext processContext = new ProcessExecutionContext<>(UUID.randomUUID(), UUID.randomUUID(), config, "test-env"); + ProcessExecutionContext processContext = new ProcessExecutionContext<>(UUID.randomUUID(), UUID.randomUUID(), config, false, "test-env"); processContext.setNetwork(network); @@ -57,7 +57,7 @@ void shouldSetAndGetNetwork() { @Test void shouldCreateStepContext() { - ProcessExecutionContext processContext = new ProcessExecutionContext<>(UUID.randomUUID(), UUID.randomUUID(), config, "test-env"); + ProcessExecutionContext processContext = new ProcessExecutionContext<>(UUID.randomUUID(), UUID.randomUUID(), config, false, "test-env"); ProcessStep step = mock(ProcessStep.class); ProcessStepType stepType = mock(ProcessStepType.class); when(stepType.getName()).thenReturn("test-step"); diff --git a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStepTest.java b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStepTest.java index 1316d04..f903ca8 100644 --- a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStepTest.java +++ b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/processes/commons/steps/ApplyModificationsStepTest.java @@ -16,9 +16,7 @@ import org.gridsuite.monitor.commons.ProcessConfig; import org.gridsuite.monitor.worker.server.core.ProcessStepExecutionContext; import org.gridsuite.monitor.worker.server.dto.ReportInfos; -import org.gridsuite.monitor.worker.server.services.FilterService; -import org.gridsuite.monitor.worker.server.services.NetworkModificationRestService; -import org.gridsuite.monitor.worker.server.services.NetworkModificationService; +import org.gridsuite.monitor.worker.server.services.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -49,6 +47,9 @@ class ApplyModificationsStepTest { @Mock private FilterService filterService; + @Mock + private S3Service s3Service; + @Mock private ProcessConfig config; @@ -62,7 +63,7 @@ class ApplyModificationsStepTest { @BeforeEach void setUp() { - applyModificationsStep = new ApplyModificationsStep<>(networkModificationService, networkModificationRestService, filterService); + applyModificationsStep = new ApplyModificationsStep<>(networkModificationService, networkModificationRestService, s3Service, filterService); when(config.modificationUuids()).thenReturn(List.of(MODIFICATION_UUID)); when(stepContext.getConfig()).thenReturn(config); ReportInfos reportInfos = new ReportInfos(REPORT_UUID, ReportNode.newRootReportNode() diff --git a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ConsumerServiceTest.java b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ConsumerServiceTest.java index c03685a..f28bee3 100644 --- a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ConsumerServiceTest.java +++ b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ConsumerServiceTest.java @@ -41,7 +41,7 @@ void setUp() { @Test void consumeRun() { - ProcessRunMessage runMessage = new ProcessRunMessage<>(UUID.randomUUID(), UUID.randomUUID(), processConfig); + ProcessRunMessage runMessage = new ProcessRunMessage<>(UUID.randomUUID(), UUID.randomUUID(), processConfig, false); Message> message = MessageBuilder.withPayload(runMessage).build(); var consumer = consumerService.consumeRun(); diff --git a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionServiceTest.java b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionServiceTest.java index c4cc51a..0071794 100644 --- a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionServiceTest.java +++ b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/ProcessExecutionServiceTest.java @@ -59,6 +59,9 @@ class ProcessExecutionServiceTest { @Mock private FilterService filterService; + @Mock + private S3Service s3Service; + @Mock private SecurityAnalysisService securityAnalysisService; @@ -80,7 +83,7 @@ void setUp() { processExecutionService = new ProcessExecutionService(processList, notificationService, EXECUTION_ENV_NAME); loadNetworkStep = new LoadNetworkStep<>(networkConversionService); - applyModificationsStep = new ApplyModificationsStep<>(networkModificationService, networkModificationRestService, filterService); + applyModificationsStep = new ApplyModificationsStep<>(networkModificationService, networkModificationRestService, s3Service, filterService); runComputationStep = new SecurityAnalysisRunComputationStep(securityAnalysisService); } @@ -90,7 +93,7 @@ void executeProcessShouldCompleteSuccessfullyWhenProcessExecutesWithoutError() { UUID caseUuid = UUID.randomUUID(); when(processConfig.processType()).thenReturn(ProcessType.SECURITY_ANALYSIS); doNothing().when(process).execute(any(ProcessExecutionContext.class)); - ProcessRunMessage runMessage = new ProcessRunMessage<>(executionId, caseUuid, processConfig); + ProcessRunMessage runMessage = new ProcessRunMessage<>(executionId, caseUuid, processConfig, false); when(process.defineSteps()).thenReturn((List) List.of(loadNetworkStep, applyModificationsStep, runComputationStep)); processExecutionService.executeProcess(runMessage); @@ -139,7 +142,7 @@ void executeProcessShouldSendFailedStatusWhenProcessThrowsException() { when(processConfig.processType()).thenReturn(ProcessType.SECURITY_ANALYSIS); RuntimeException processException = new RuntimeException("Process execution failed"); doThrow(processException).when(process).execute(any(ProcessExecutionContext.class)); - ProcessRunMessage runMessage = new ProcessRunMessage<>(executionId, caseUuid, processConfig); + ProcessRunMessage runMessage = new ProcessRunMessage<>(executionId, caseUuid, processConfig, false); assertThrows(RuntimeException.class, () -> processExecutionService.executeProcess(runMessage)); @@ -158,7 +161,7 @@ void executeProcessShouldSendFailedStatusWhenProcessThrowsException() { @Test void executeProcessShouldThrowIllegalArgumentExceptionWhenProcessTypeNotFound() { when(processConfig.processType()).thenReturn(null); - ProcessRunMessage runMessage = new ProcessRunMessage<>(UUID.randomUUID(), UUID.randomUUID(), processConfig); + ProcessRunMessage runMessage = new ProcessRunMessage<>(UUID.randomUUID(), UUID.randomUUID(), processConfig, false); assertThatThrownBy(() -> processExecutionService.executeProcess(runMessage)) .isInstanceOf(IllegalArgumentException.class) diff --git a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/S3RestServiceTest.java b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/S3RestServiceTest.java new file mode 100644 index 0000000..6e598b5 --- /dev/null +++ b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/S3RestServiceTest.java @@ -0,0 +1,81 @@ +package org.gridsuite.monitor.worker.server.services; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class S3RestServiceTest { + @Mock + S3Client s3Client; + + S3RestService s3RestService; + + @TempDir + Path testDir; + + @BeforeEach + void setup() { + s3RestService = new S3RestService(s3Client, "my-bucket"); + } + + @Test + void testUploadFileToS3() throws Exception { + Path tempFile = Files.createTempFile(testDir, "test", ".txt"); + Files.writeString(tempFile, "dataToUpload"); + + s3RestService.uploadFile(tempFile, "testS3Key", "fileToUploadName"); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(PutObjectRequest.class); + + verify(s3Client).putObject( + requestCaptor.capture(), + any(RequestBody.class) + ); + + PutObjectRequest request = requestCaptor.getValue(); + + assertThat(request.bucket()).isEqualTo("my-bucket"); + assertThat(request.key()).isEqualTo("testS3Key"); + assertThat(request.metadata()).containsEntry("file-name", "fileToUploadName"); + } + + @Test + void testUpdateFileToS3Error() throws Exception { + + Path fileToUpload = Files.createTempFile(testDir, "test", ".txt"); + Files.writeString(fileToUpload, "dataToUpload"); + + doThrow(SdkException.builder().message("sdkError").build()) + .when(s3Client) + .putObject(any(PutObjectRequest.class), any(RequestBody.class)); + + assertThatThrownBy(() -> + s3RestService.uploadFile(fileToUpload, "key", "file.txt") + ) + .isInstanceOf(IOException.class) + .hasMessageContaining("Error occurred while uploading file to S3") + .hasMessageContaining("sdkError"); + + verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); + } +} diff --git a/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/S3ServiceTest.java b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/S3ServiceTest.java new file mode 100644 index 0000000..dc3df45 --- /dev/null +++ b/monitor-worker-server/src/test/java/org/gridsuite/monitor/worker/server/services/S3ServiceTest.java @@ -0,0 +1,95 @@ +package org.gridsuite.monitor.worker.server.services; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.util.function.ThrowingConsumer; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPInputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class S3ServiceTest { + @Mock + S3RestService s3RestService; + + @InjectMocks + S3Service s3Service; + + @TempDir + Path testDir; + + @Test + void testPathIsCompressedBeforeUploading() throws Exception { + // --- Inputs --- + String s3Key = "s3Key"; + String fileName = "fileName"; + String dataToCompress = "dataToCompress"; + ThrowingConsumer writer = + path -> Files.writeString(path, dataToCompress); + + // Since temp files are deleted after "uploadFile", make a copy of the compressed file to assert its content + Path capturedCopy = Files.createTempFile(testDir, "test-copy", ".gz"); + doAnswer(invocation -> { + Path uploaded = invocation.getArgument(0); + Files.copy(uploaded, capturedCopy, StandardCopyOption.REPLACE_EXISTING); + return null; + }).when(s3RestService) + .uploadFile(any(Path.class), anyString(), anyString()); + + // --- Method invocation --- + s3Service.exportCompressedToS3(s3Key, fileName, writer); + + // --- Assertions --- + verify(s3RestService).uploadFile(any(), eq(s3Key), eq(fileName)); + try (InputStream in = new GZIPInputStream(Files.newInputStream(capturedCopy))) { + String uncompressedContent = new String(in.readAllBytes(), UTF_8); + assertThat(uncompressedContent).isEqualTo(dataToCompress); + } + } + + @Test + void testTempFilesAreDeletedAfterExecution() throws Exception { + // --- Inputs --- + AtomicReference debugFileToUplad = new AtomicReference<>(); + AtomicReference debugTempDir = new AtomicReference<>(); + String s3Key = "s3Key"; + String fileName = "fileName"; + + ThrowingConsumer writer = path -> { + debugFileToUplad.set(path); + debugTempDir.set(path.getParent()); + Files.writeString(path, "hello"); + }; + + // --- Method invocation --- + s3Service.exportCompressedToS3(s3Key, fileName, writer); + + // --- Assertions --- + ArgumentCaptor compressedDebugFileToUploadCaptor = ArgumentCaptor.forClass(Path.class); + verify(s3RestService).uploadFile( + compressedDebugFileToUploadCaptor.capture(), + eq(s3Key), + eq(fileName) + ); + Path compressedDebugFileToUpload = compressedDebugFileToUploadCaptor.getValue(); + + assertThat(Files.exists(compressedDebugFileToUpload)).isFalse(); + assertThat(Files.exists(debugFileToUplad.get())).isFalse(); + assertThat(Files.exists(debugTempDir.get())).isFalse(); + } +}