From 9b8b8dca9d9e75c52ef5525d0830941818787297 Mon Sep 17 00:00:00 2001 From: LE SAULNIER Kevin Date: Wed, 14 Jan 2026 11:04:30 +0100 Subject: [PATCH 1/7] feat: add endpoint to save sa result without enriching them with data from network Signed-off-by: LE SAULNIER Kevin --- .../server/SecurityAnalysisController.java | 9 +++++ .../server/entities/ContingencyEntity.java | 3 +- .../ContingencyLimitViolationEntity.java | 39 +++++++++++++------ .../PreContingencyLimitViolationEntity.java | 35 +++++++++++++---- .../SecurityAnalysisResultEntity.java | 7 +++- .../SecurityAnalysisResultService.java | 9 +++++ 6 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java index 5ee6e33a..a18b358e 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java @@ -247,6 +247,15 @@ public ResponseEntity stop(@Parameter(description = "Result UUID") @PathVa return ResponseEntity.ok().build(); } + @PostMapping(value = "/results/{resultUuid}", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE) + @Operation(summary = "Save security analysis results") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis results have been saved to database")}) + public ResponseEntity saveResult(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid, + @RequestBody SecurityAnalysisResult result) { + securityAnalysisResultService.insert(resultUuid, result, SecurityAnalysisStatus.CONVERGED); + return ResponseEntity.ok().build(); + } + @GetMapping(value = "/providers", produces = APPLICATION_JSON_VALUE) @Operation(summary = "Get all security analysis providers") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Security analysis providers have been found")}) diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyEntity.java index 21e7a562..2c8f5656 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyEntity.java @@ -13,6 +13,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.FieldNameConstants; +import org.springframework.lang.Nullable; import java.util.List; import java.util.Map; @@ -64,7 +65,7 @@ private void setContingencyLimitViolations(List } } - public static ContingencyEntity toEntity(Network network, PostContingencyResult postContingencyResult, Map subjectLimitViolationsBySubjectId) { + public static ContingencyEntity toEntity(@Nullable Network network, PostContingencyResult postContingencyResult, Map subjectLimitViolationsBySubjectId) { List contingencyElements = postContingencyResult.getContingency().getElements().stream().map(contingencyElement -> ContingencyElementEmbeddable.toEntity(contingencyElement)).collect(Collectors.toList()); List contingencyLimitViolations = postContingencyResult.getLimitViolationsResult().getLimitViolations().stream() diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java index 85e4dbb5..d6bdc8e8 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java @@ -13,6 +13,7 @@ import lombok.*; import lombok.experimental.FieldNameConstants; import lombok.experimental.SuperBuilder; +import org.springframework.lang.Nullable; /** * @author Kevin Le Saulnier @@ -30,10 +31,8 @@ public class ContingencyLimitViolationEntity extends AbstractLimitViolationEntit @Setter private ContingencyEntity contingency; - public static ContingencyLimitViolationEntity toEntity(Network network, LimitViolation limitViolation, SubjectLimitViolationEntity subjectLimitViolation) { - Double patlLimit = getPatlLimit(limitViolation, network); - - ContingencyLimitViolationEntity contingencyLimitViolationEntity = ContingencyLimitViolationEntity.builder() + public static ContingencyLimitViolationEntity toEntity(@Nullable Network network, LimitViolation limitViolation, SubjectLimitViolationEntity subjectLimitViolation) { + ContingencyLimitViolationEntityBuilder contingencyLimitViolationEntityBuilder = ContingencyLimitViolationEntity.builder() .limit(limitViolation.getLimit()) .limitName(limitViolation.getLimitName()) .limitType(limitViolation.getLimitType()) @@ -41,17 +40,35 @@ public static ContingencyLimitViolationEntity toEntity(Network network, LimitVio .value(limitViolation.getValue()) .side(limitViolation.getSide()) .loading(computeLoading(limitViolation, limitViolation.getLimit())) - .locationId(ComputationResultUtils.getViolationLocationId(limitViolation, network)) .subjectLimitViolation(subjectLimitViolation) - .patlLimit(patlLimit) - .patlLoading(computeLoading(limitViolation, patlLimit)) - .nextLimitName(getNextLimitName(limitViolation, network)) - .acceptableDuration(calculateActualOverloadDuration(limitViolation, network)) - .upcomingAcceptableDuration(calculateUpcomingOverloadDuration(limitViolation)) - .build(); + .upcomingAcceptableDuration(calculateUpcomingOverloadDuration(limitViolation)); + + if (network != null) { + enrichBuilderWithNetworkData(contingencyLimitViolationEntityBuilder, network, limitViolation); + } else { + enrichBuilderWithoutNetworkData(contingencyLimitViolationEntityBuilder); + } + + ContingencyLimitViolationEntity contingencyLimitViolationEntity = contingencyLimitViolationEntityBuilder.build(); subjectLimitViolation.addContingencyLimitViolation(contingencyLimitViolationEntity); return contingencyLimitViolationEntity; } + + private static void enrichBuilderWithNetworkData(ContingencyLimitViolationEntityBuilder contingencyLimitViolationEntityBuilder, Network network, LimitViolation limitViolation) { + Double patlLimit = getPatlLimit(limitViolation, network); + contingencyLimitViolationEntityBuilder + .patlLimit(patlLimit) + .patlLoading(computeLoading(limitViolation, patlLimit)) + .nextLimitName(getNextLimitName(limitViolation, network)) + .locationId(ComputationResultUtils.getViolationLocationId(limitViolation, network)) + .acceptableDuration(calculateActualOverloadDuration(limitViolation, network)); + } + + private static void enrichBuilderWithoutNetworkData(ContingencyLimitViolationEntityBuilder contingencyLimitViolationEntityBuilder) { + // acceptable duration in not nullable + contingencyLimitViolationEntityBuilder + .acceptableDuration(0); + } } diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java index 061ddf0d..966276e4 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java @@ -14,6 +14,7 @@ import jakarta.persistence.*; import lombok.experimental.FieldNameConstants; import lombok.experimental.SuperBuilder; +import org.springframework.lang.Nullable; import java.util.List; import java.util.Map; @@ -35,27 +36,45 @@ public class PreContingencyLimitViolationEntity extends AbstractLimitViolationEn @Setter SecurityAnalysisResultEntity result; - public static List toEntityList(Network network, PreContingencyResult preContingencyResult, Map subjectLimitViolationsBySubjectId) { + public static List toEntityList(@Nullable Network network, PreContingencyResult preContingencyResult, Map subjectLimitViolationsBySubjectId) { return preContingencyResult.getLimitViolationsResult().getLimitViolations().stream().map(limitViolation -> toEntity(network, limitViolation, subjectLimitViolationsBySubjectId.get(limitViolation.getSubjectId()))).collect(Collectors.toList()); } - public static PreContingencyLimitViolationEntity toEntity(Network network, LimitViolation limitViolation, SubjectLimitViolationEntity subjectLimitViolation) { - Double patlLimit = getPatlLimit(limitViolation, network); - return PreContingencyLimitViolationEntity.builder() + public static PreContingencyLimitViolationEntity toEntity(@Nullable Network network, LimitViolation limitViolation, SubjectLimitViolationEntity subjectLimitViolation) { + PreContingencyLimitViolationEntityBuilder preContingencyLimitViolationEntityBuilder = PreContingencyLimitViolationEntity.builder() .subjectLimitViolation(subjectLimitViolation) .limit(limitViolation.getLimit()) .limitName(limitViolation.getLimitName()) .limitType(limitViolation.getLimitType()) - .acceptableDuration(calculateActualOverloadDuration(limitViolation, network)) .upcomingAcceptableDuration(calculateUpcomingOverloadDuration(limitViolation)) .limitReduction(limitViolation.getLimitReduction()) .value(limitViolation.getValue()) .side(limitViolation.getSide()) + .loading(computeLoading(limitViolation, limitViolation.getLimit())); + + if (network != null) { + enrichBuilderWithNetworkData(preContingencyLimitViolationEntityBuilder, network, limitViolation); + } else { + enrichBuilderWithoutNetworkData(preContingencyLimitViolationEntityBuilder); + } + + return preContingencyLimitViolationEntityBuilder.build(); + } + + private static void enrichBuilderWithNetworkData(PreContingencyLimitViolationEntity.PreContingencyLimitViolationEntityBuilder preContingencyLimitViolationEntityBuilder, Network network, LimitViolation limitViolation) { + Double patlLimit = getPatlLimit(limitViolation, network); + + preContingencyLimitViolationEntityBuilder + .acceptableDuration(calculateActualOverloadDuration(limitViolation, network)) .patlLimit(patlLimit) - .loading(computeLoading(limitViolation, limitViolation.getLimit())) .patlLoading(computeLoading(limitViolation, patlLimit)) .locationId(ComputationResultUtils.getViolationLocationId(limitViolation, network)) - .nextLimitName(getNextLimitName(limitViolation, network)) - .build(); + .nextLimitName(getNextLimitName(limitViolation, network)); + } + + private static void enrichBuilderWithoutNetworkData(PreContingencyLimitViolationEntity.PreContingencyLimitViolationEntityBuilder preContingencyLimitViolationEntityBuilder) { + // acceptable duration in not nullable + preContingencyLimitViolationEntityBuilder + .acceptableDuration(0); } } diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java index 6d192143..1aa4b837 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java @@ -13,6 +13,7 @@ import lombok.experimental.FieldNameConstants; import org.gridsuite.securityanalysis.server.dto.SecurityAnalysisStatus; import org.jgrapht.alg.util.Pair; +import org.springframework.lang.Nullable; import java.util.List; import java.util.Map; @@ -53,7 +54,7 @@ public SecurityAnalysisResultEntity(UUID id) { this.id = id; } - public static SecurityAnalysisResultEntity toEntity(Network network, UUID resultUuid, SecurityAnalysisResult securityAnalysisResult, SecurityAnalysisStatus securityAnalysisStatus) { + public static SecurityAnalysisResultEntity toEntity(@Nullable Network network, UUID resultUuid, SecurityAnalysisResult securityAnalysisResult, SecurityAnalysisStatus securityAnalysisStatus) { Map subjectLimitViolationsBySubjectId = getUniqueSubjectLimitViolationsFromResult(securityAnalysisResult) .stream().collect(Collectors.toMap( SubjectLimitViolationEntity::getSubjectId, @@ -89,6 +90,10 @@ public static SecurityAnalysisResultEntity toEntity(Network network, UUID result return securityAnalysisResultEntity; } + public static SecurityAnalysisResultEntity toEntity(UUID resultUuid, SecurityAnalysisResult securityAnalysisResult, SecurityAnalysisStatus securityAnalysisStatus) { + return toEntity(null, resultUuid, securityAnalysisResult, securityAnalysisStatus); + } + private static List getUniqueSubjectLimitViolationsFromResult(SecurityAnalysisResult securityAnalysisResult) { return Stream.concat( securityAnalysisResult.getPostContingencyResults().stream().flatMap(pcr -> pcr.getLimitViolationsResult().getLimitViolations().stream()), diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java index cfdbdd60..0a7f3fc5 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java @@ -246,6 +246,15 @@ public void insert(Network network, UUID resultUuid, SecurityAnalysisResult resu securityAnalysisResultRepository.save(securityAnalysisResult); } + @Transactional + public void insert(UUID resultUuid, SecurityAnalysisResult result, SecurityAnalysisStatus status) { + Objects.requireNonNull(resultUuid); + Objects.requireNonNull(result); + + SecurityAnalysisResultEntity securityAnalysisResult = SecurityAnalysisResultEntity.toEntity(resultUuid, result, status); + securityAnalysisResultRepository.save(securityAnalysisResult); + } + @Override @Transactional public void insertStatus(List resultUuids, SecurityAnalysisStatus status) { From 86e100006689fdecfd4d7df729044b6747eac606 Mon Sep 17 00:00:00 2001 From: LE SAULNIER Kevin Date: Wed, 14 Jan 2026 14:19:42 +0100 Subject: [PATCH 2/7] test coverage + simplify code Signed-off-by: LE SAULNIER Kevin --- .../server/SecurityAnalysisController.java | 2 +- .../SecurityAnalysisResultEntity.java | 4 -- .../SecurityAnalysisResultService.java | 12 +----- .../SecurityAnalysisControllerTest.java | 26 +++++++++++++ .../SecurityAnalysisResultServiceTest.java | 38 +++++++++++++++++++ 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java index a18b358e..2cf430f9 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java @@ -252,7 +252,7 @@ public ResponseEntity stop(@Parameter(description = "Result UUID") @PathVa @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis results have been saved to database")}) public ResponseEntity saveResult(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid, @RequestBody SecurityAnalysisResult result) { - securityAnalysisResultService.insert(resultUuid, result, SecurityAnalysisStatus.CONVERGED); + securityAnalysisResultService.insert(null, resultUuid, result, SecurityAnalysisStatus.CONVERGED); return ResponseEntity.ok().build(); } diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java index 1aa4b837..470fe5fe 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/SecurityAnalysisResultEntity.java @@ -90,10 +90,6 @@ public static SecurityAnalysisResultEntity toEntity(@Nullable Network network, U return securityAnalysisResultEntity; } - public static SecurityAnalysisResultEntity toEntity(UUID resultUuid, SecurityAnalysisResult securityAnalysisResult, SecurityAnalysisStatus securityAnalysisStatus) { - return toEntity(null, resultUuid, securityAnalysisResult, securityAnalysisStatus); - } - private static List getUniqueSubjectLimitViolationsFromResult(SecurityAnalysisResult securityAnalysisResult) { return Stream.concat( securityAnalysisResult.getPostContingencyResults().stream().flatMap(pcr -> pcr.getLimitViolationsResult().getLimitViolations().stream()), diff --git a/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java index 0a7f3fc5..fd020cc8 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultService.java @@ -29,6 +29,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.*; import org.springframework.data.jpa.domain.Specification; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -238,7 +239,7 @@ public void assertResultExists(UUID resultUuid) { } @Transactional - public void insert(Network network, UUID resultUuid, SecurityAnalysisResult result, SecurityAnalysisStatus status) { + public void insert(@Nullable Network network, UUID resultUuid, SecurityAnalysisResult result, SecurityAnalysisStatus status) { Objects.requireNonNull(resultUuid); Objects.requireNonNull(result); @@ -246,15 +247,6 @@ public void insert(Network network, UUID resultUuid, SecurityAnalysisResult resu securityAnalysisResultRepository.save(securityAnalysisResult); } - @Transactional - public void insert(UUID resultUuid, SecurityAnalysisResult result, SecurityAnalysisStatus status) { - Objects.requireNonNull(resultUuid); - Objects.requireNonNull(result); - - SecurityAnalysisResultEntity securityAnalysisResult = SecurityAnalysisResultEntity.toEntity(resultUuid, result, status); - securityAnalysisResultRepository.save(securityAnalysisResult); - } - @Override @Transactional public void insertStatus(List resultUuids, SecurityAnalysisStatus status) { diff --git a/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java b/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java index 82cb02ae..8b961d16 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java @@ -36,6 +36,7 @@ import org.gridsuite.securityanalysis.server.repositories.SubjectLimitViolationRepository; import org.gridsuite.securityanalysis.server.service.ActionsService; import org.gridsuite.securityanalysis.server.service.LoadFlowService; +import org.gridsuite.securityanalysis.server.service.SecurityAnalysisResultService; import org.gridsuite.securityanalysis.server.service.SecurityAnalysisWorkerService; import org.gridsuite.securityanalysis.server.util.ContextConfigurationWithTestChannel; import org.gridsuite.securityanalysis.server.util.CsvExportUtils; @@ -43,6 +44,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.MockitoAnnotations; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +56,7 @@ import org.springframework.http.MediaType; import org.springframework.messaging.Message; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -143,6 +146,9 @@ class SecurityAnalysisControllerTest { @Autowired private SubjectLimitViolationRepository subjectLimitViolationRepository; + @MockitoSpyBean + private SecurityAnalysisResultService securityAnalysisResultService; + private static final Map ENUM_TRANSLATIONS_EN = Map.of( "ONE", "Side 1", "TWO", "Side 2", @@ -838,6 +844,26 @@ void getDefaultProviderTest() throws Exception { ); } + @Test + void saveResultTest() throws Exception { + UUID resultUuid = UUID.randomUUID(); + SecurityAnalysisResult securityAnalysisResult = SecurityAnalysisProviderMock.RESULT; + doNothing().when(securityAnalysisResultService).insert(null, resultUuid, securityAnalysisResult, SecurityAnalysisStatus.CONVERGED); + + mockMvc.perform(post("/" + VERSION + "/results/" + resultUuid) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(securityAnalysisResult))) + .andExpect(status().isOk()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SecurityAnalysisResult.class); + verify(securityAnalysisResultService, times(1)).insert(eq(null), eq(resultUuid), captor.capture(), eq(SecurityAnalysisStatus.CONVERGED)); + Assertions.assertThat(captor.getValue()) + .usingRecursiveComparison() + // this field is not well serialized/deserialized - since it's deprecated / not used, we ignore it here + .ignoringFieldsMatchingRegexes(".*\\.limitViolationsResult\\.computationOk") + .isEqualTo(securityAnalysisResult); + } + @Test void getZippedCsvResults() throws Exception { // running computation to create some results diff --git a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java index c2ea216b..4d198ad5 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java @@ -10,15 +10,22 @@ import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; import com.powsybl.network.store.iidm.impl.NetworkFactoryImpl; import com.vladmihalcea.sql.SQLStatementCountValidator; +import org.gridsuite.securityanalysis.server.dto.ContingencyResultDTO; import org.gridsuite.securityanalysis.server.dto.SecurityAnalysisStatus; +import org.gridsuite.securityanalysis.server.dto.SubjectLimitViolationDTO; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import java.util.List; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThat; import static org.gridsuite.securityanalysis.server.SecurityAnalysisProviderMock.*; import static org.gridsuite.securityanalysis.server.util.DatabaseQueryUtils.assertRequestsCount; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author Florent MILLOT @@ -41,4 +48,35 @@ void deleteResultPerfTest() { // 1 manual select to get the contingencyUuids, and 4 select at the end for the last delete when applying the cascade assertRequestsCount(5, 0, 0, 6); } + + @Test + void insertResultWithoutNetworkTest() { + UUID resultUuid = UUID.randomUUID(); + securityAnalysisResultService.insert(null, resultUuid, RESULT, SecurityAnalysisStatus.CONVERGED); + securityAnalysisResultService.assertResultExists(resultUuid); + + List contingencyResults = securityAnalysisResultService.findNmKContingenciesResult(resultUuid); + assertEquals(RESULT.getPostContingencyResults().size(), contingencyResults.size()); + // check fields based on network are actually nullish + contingencyResults.forEach(this::checkFieldBasedOnNetworkAreNullish); + } + + private void checkFieldBasedOnNetworkAreNullish(ContingencyResultDTO contingencyResult) { + assertThat(contingencyResult.getSubjectLimitViolations()) + .extracting(SubjectLimitViolationDTO::getLimitViolation) + .allSatisfy(lv -> + assertThat(lv) + .as("limitViolation") + .satisfies(v -> assertThat(v.getPatlLimit()).isNull()) + .satisfies(v -> assertThat(v.getPatlLoading()).isNull()) + .satisfies(v -> assertThat(v.getNextLimitName()).isNull()) + .satisfies(v -> assertThat(v.getLocationId()).isNull()) + .satisfies(v -> assertThat(v.getAcceptableDuration()).isZero()) + ); + } + + @AfterEach + void tearDown() { + securityAnalysisResultService.deleteAll(); + } } From d6262a8dcd20365d8b59cdd5d5810ee0acc5c184 Mon Sep 17 00:00:00 2001 From: LE SAULNIER Kevin Date: Wed, 14 Jan 2026 14:28:06 +0100 Subject: [PATCH 3/7] fix: checkstyle Signed-off-by: LE SAULNIER Kevin --- .../server/service/SecurityAnalysisResultServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java index 4d198ad5..c79cbe01 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java @@ -25,7 +25,6 @@ import static org.gridsuite.securityanalysis.server.SecurityAnalysisProviderMock.*; import static org.gridsuite.securityanalysis.server.util.DatabaseQueryUtils.assertRequestsCount; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author Florent MILLOT From b977ed58ae928a2ea52b76fc0bea20b9b05c49d7 Mon Sep 17 00:00:00 2001 From: LE SAULNIER Kevin Date: Wed, 14 Jan 2026 14:39:42 +0100 Subject: [PATCH 4/7] fix: suppress false warning Signed-off-by: LE SAULNIER Kevin --- .../server/service/SecurityAnalysisResultServiceTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java index c79cbe01..cd0f0d5b 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java @@ -60,6 +60,7 @@ void insertResultWithoutNetworkTest() { contingencyResults.forEach(this::checkFieldBasedOnNetworkAreNullish); } + @SuppressWarnings("java:S5841") // some limitViolation could be empty, which will make allSatisfy pass automatically - this behaviour is intended private void checkFieldBasedOnNetworkAreNullish(ContingencyResultDTO contingencyResult) { assertThat(contingencyResult.getSubjectLimitViolations()) .extracting(SubjectLimitViolationDTO::getLimitViolation) From 5eb43c0a36e6fc10b48d27eca9e1c70e5c4e314c Mon Sep 17 00:00:00 2001 From: LE SAULNIER Kevin Date: Wed, 14 Jan 2026 15:00:19 +0100 Subject: [PATCH 5/7] fix: set acceptable duration to max_int instead of 0 if null Signed-off-by: LE SAULNIER Kevin --- .../server/entities/ContingencyLimitViolationEntity.java | 4 ++-- .../server/entities/PreContingencyLimitViolationEntity.java | 4 ++-- .../server/service/SecurityAnalysisResultServiceTest.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java index d6bdc8e8..3f6c50f3 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java @@ -67,8 +67,8 @@ private static void enrichBuilderWithNetworkData(ContingencyLimitViolationEntity } private static void enrichBuilderWithoutNetworkData(ContingencyLimitViolationEntityBuilder contingencyLimitViolationEntityBuilder) { - // acceptable duration in not nullable + // acceptable duration in not nullable - in other cases, when null, it is set to Integer.MAX_VALUE contingencyLimitViolationEntityBuilder - .acceptableDuration(0); + .acceptableDuration(Integer.MAX_VALUE); } } diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java index 966276e4..737a1f26 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java @@ -73,8 +73,8 @@ private static void enrichBuilderWithNetworkData(PreContingencyLimitViolationEnt } private static void enrichBuilderWithoutNetworkData(PreContingencyLimitViolationEntity.PreContingencyLimitViolationEntityBuilder preContingencyLimitViolationEntityBuilder) { - // acceptable duration in not nullable + // acceptable duration in not nullable - in other cases, when null, it is set to Integer.MAX_VALUE preContingencyLimitViolationEntityBuilder - .acceptableDuration(0); + .acceptableDuration(Integer.MAX_VALUE); } } diff --git a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java index cd0f0d5b..4446d960 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java @@ -71,7 +71,7 @@ private void checkFieldBasedOnNetworkAreNullish(ContingencyResultDTO contingency .satisfies(v -> assertThat(v.getPatlLoading()).isNull()) .satisfies(v -> assertThat(v.getNextLimitName()).isNull()) .satisfies(v -> assertThat(v.getLocationId()).isNull()) - .satisfies(v -> assertThat(v.getAcceptableDuration()).isZero()) + .satisfies(v -> assertThat(v.getAcceptableDuration()).isEqualTo(Integer.MAX_VALUE)) ); } From 956345bda118ecf0123fc24cec1440b26fa70959 Mon Sep 17 00:00:00 2001 From: LE SAULNIER Kevin Date: Thu, 15 Jan 2026 10:09:20 +0100 Subject: [PATCH 6/7] feat: add endpoint to get unpaged nmk results Signed-off-by: LE SAULNIER Kevin --- .../server/SecurityAnalysisController.java | 24 +++++++++---- .../SecurityAnalysisControllerTest.java | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java index 2cf430f9..b661b223 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java @@ -158,12 +158,12 @@ public ResponseEntity getNResultZippedCsv(@Parameter(description = "Resu @Operation(summary = "Get a security analysis result from the database - NMK contingencies result") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis result"), @ApiResponse(responseCode = "404", description = "Security analysis result has not been found")}) - public ResponseEntity> getNmKContingenciesResult(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid, - @Parameter(description = "network Uuid") @RequestParam(name = "networkUuid", required = false) UUID networkUuid, - @Parameter(description = "variant Id") @RequestParam(name = "variantId", required = false) String variantId, - @Parameter(description = "Filters") @RequestParam(name = "filters", required = false) String stringFilters, - @Parameter(description = "Global Filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, - @Parameter(description = "Pagination parameters") Pageable pageable) { + public ResponseEntity> getPagedNmKContingenciesResult(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid, + @Parameter(description = "network Uuid") @RequestParam(name = "networkUuid", required = false) UUID networkUuid, + @Parameter(description = "variant Id") @RequestParam(name = "variantId", required = false) String variantId, + @Parameter(description = "Filters") @RequestParam(name = "filters", required = false) String stringFilters, + @Parameter(description = "Global Filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, + @Parameter(description = "Pagination parameters") Pageable pageable) { String decodedStringFilters = stringFilters != null ? URLDecoder.decode(stringFilters, StandardCharsets.UTF_8) : null; String decodedStringGlobalFilters = globalFilters != null ? URLDecoder.decode(globalFilters, StandardCharsets.UTF_8) : null; Page result = securityAnalysisResultService.findNmKContingenciesPaged(resultUuid, networkUuid, variantId, decodedStringFilters, decodedStringGlobalFilters, pageable); @@ -173,6 +173,18 @@ public ResponseEntity> getNmKContingenciesResult(@Par : ResponseEntity.notFound().build(); } + @GetMapping(value = "/results/{resultUuid}/nmk-contingencies-result", produces = APPLICATION_JSON_VALUE) + @Operation(summary = "Get a security analysis result from the database - NMK contingencies result") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis result"), + @ApiResponse(responseCode = "404", description = "Security analysis result has not been found")}) + public ResponseEntity> getNmKContingenciesResult(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid) { + List result = securityAnalysisResultService.findNmKContingenciesResult(resultUuid); + + return result != null + ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result) + : ResponseEntity.notFound().build(); + } + @PostMapping(value = "/results/{resultUuid}/nmk-contingencies-result/csv", produces = APPLICATION_OCTET_STREAM_VALUE, consumes = APPLICATION_JSON_VALUE) @Operation(summary = "Get a security analysis result from the database - NMK contingencies result - CSV export") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis result csv export"), diff --git a/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java b/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java index 8b961d16..a507adf1 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/SecurityAnalysisControllerTest.java @@ -26,6 +26,7 @@ import org.assertj.core.api.Assertions; import org.gridsuite.computation.dto.GlobalFilter; import org.gridsuite.computation.dto.ResourceFilterDTO; +import org.gridsuite.computation.error.ComputationException; import org.gridsuite.computation.service.AbstractFilterService; import org.gridsuite.computation.service.ReportService; import org.gridsuite.computation.service.UuidGeneratorService; @@ -75,6 +76,7 @@ import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.powsybl.network.store.model.NetworkStoreApi.VERSION; +import static org.gridsuite.computation.error.ComputationBusinessErrorCode.RESULT_NOT_FOUND; import static org.gridsuite.computation.service.NotificationService.*; import static org.gridsuite.securityanalysis.server.SecurityAnalysisProviderMock.*; import static org.gridsuite.securityanalysis.server.service.SecurityAnalysisService.COMPUTATION_TYPE; @@ -864,6 +866,38 @@ void saveResultTest() throws Exception { .isEqualTo(securityAnalysisResult); } + @Test + void getNmKContingenciesResult() throws Exception { + UUID resultUuid = UUID.randomUUID(); + + List serviceResult = SecurityAnalysisProviderMock.RESULT_CONTINGENCIES; + + doReturn(serviceResult).when(securityAnalysisResultService).findNmKContingenciesResult(resultUuid); + + mockMvc.perform(get("/" + VERSION + "/results/" + resultUuid + "/nmk-contingencies-result") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(mapper.writeValueAsString(serviceResult))); + + verify(securityAnalysisResultService, times(1)) + .findNmKContingenciesResult(resultUuid); + } + + @Test + void getNmKContingenciesResultNotFound() throws Exception { + UUID resultUuid = UUID.randomUUID(); + + doReturn(null).when(securityAnalysisResultService).findNmKContingenciesResult(resultUuid); + + mockMvc.perform(get("/" + VERSION + "/results/" + resultUuid + "/nmk-contingencies-result") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + verify(securityAnalysisResultService, times(1)) + .findNmKContingenciesResult(resultUuid); + } + @Test void getZippedCsvResults() throws Exception { // running computation to create some results From fb76b583ae1afe6dfe4b2f9f434da02352c4b6cb Mon Sep 17 00:00:00 2001 From: LE SAULNIER Kevin Date: Fri, 16 Jan 2026 13:49:29 +0100 Subject: [PATCH 7/7] fix: PR remarks Signed-off-by: LE SAULNIER Kevin --- .../server/SecurityAnalysisController.java | 12 ++++++++++-- .../entities/ContingencyLimitViolationEntity.java | 8 -------- .../entities/PreContingencyLimitViolationEntity.java | 8 -------- .../changesets/changelog_20260116T110615Z.xml | 9 +++++++++ .../resources/db/changelog/db.changelog-master.yaml | 3 +++ .../service/SecurityAnalysisResultServiceTest.java | 3 +-- 6 files changed, 23 insertions(+), 20 deletions(-) create mode 100644 src/main/resources/db/changelog/changesets/changelog_20260116T110615Z.xml diff --git a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java index 2cf430f9..f646d15e 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/SecurityAnalysisController.java @@ -7,6 +7,7 @@ package org.gridsuite.securityanalysis.server; import com.powsybl.iidm.network.ThreeSides; +import com.powsybl.loadflow.LoadFlowResult; import com.powsybl.security.LimitViolationType; import com.powsybl.security.SecurityAnalysisResult; import io.swagger.v3.oas.annotations.Operation; @@ -249,10 +250,17 @@ public ResponseEntity stop(@Parameter(description = "Result UUID") @PathVa @PostMapping(value = "/results/{resultUuid}", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE) @Operation(summary = "Save security analysis results") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis results have been saved to database")}) + @ApiResponses(value = {@ApiResponse(responseCode = "201", description = "The security analysis results have been saved to database")}) public ResponseEntity saveResult(@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid, @RequestBody SecurityAnalysisResult result) { - securityAnalysisResultService.insert(null, resultUuid, result, SecurityAnalysisStatus.CONVERGED); + securityAnalysisResultService.insert( + null, + resultUuid, + result, + result.getPreContingencyResult().getStatus() == LoadFlowResult.ComponentResult.Status.CONVERGED + ? SecurityAnalysisStatus.CONVERGED + : SecurityAnalysisStatus.DIVERGED + ); return ResponseEntity.ok().build(); } diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java index 3f6c50f3..426316bb 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/ContingencyLimitViolationEntity.java @@ -45,8 +45,6 @@ public static ContingencyLimitViolationEntity toEntity(@Nullable Network network if (network != null) { enrichBuilderWithNetworkData(contingencyLimitViolationEntityBuilder, network, limitViolation); - } else { - enrichBuilderWithoutNetworkData(contingencyLimitViolationEntityBuilder); } ContingencyLimitViolationEntity contingencyLimitViolationEntity = contingencyLimitViolationEntityBuilder.build(); @@ -65,10 +63,4 @@ private static void enrichBuilderWithNetworkData(ContingencyLimitViolationEntity .locationId(ComputationResultUtils.getViolationLocationId(limitViolation, network)) .acceptableDuration(calculateActualOverloadDuration(limitViolation, network)); } - - private static void enrichBuilderWithoutNetworkData(ContingencyLimitViolationEntityBuilder contingencyLimitViolationEntityBuilder) { - // acceptable duration in not nullable - in other cases, when null, it is set to Integer.MAX_VALUE - contingencyLimitViolationEntityBuilder - .acceptableDuration(Integer.MAX_VALUE); - } } diff --git a/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java b/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java index 737a1f26..f860c3d7 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/entities/PreContingencyLimitViolationEntity.java @@ -54,8 +54,6 @@ public static PreContingencyLimitViolationEntity toEntity(@Nullable Network netw if (network != null) { enrichBuilderWithNetworkData(preContingencyLimitViolationEntityBuilder, network, limitViolation); - } else { - enrichBuilderWithoutNetworkData(preContingencyLimitViolationEntityBuilder); } return preContingencyLimitViolationEntityBuilder.build(); @@ -71,10 +69,4 @@ private static void enrichBuilderWithNetworkData(PreContingencyLimitViolationEnt .locationId(ComputationResultUtils.getViolationLocationId(limitViolation, network)) .nextLimitName(getNextLimitName(limitViolation, network)); } - - private static void enrichBuilderWithoutNetworkData(PreContingencyLimitViolationEntity.PreContingencyLimitViolationEntityBuilder preContingencyLimitViolationEntityBuilder) { - // acceptable duration in not nullable - in other cases, when null, it is set to Integer.MAX_VALUE - preContingencyLimitViolationEntityBuilder - .acceptableDuration(Integer.MAX_VALUE); - } } diff --git a/src/main/resources/db/changelog/changesets/changelog_20260116T110615Z.xml b/src/main/resources/db/changelog/changesets/changelog_20260116T110615Z.xml new file mode 100644 index 00000000..a2b0bab9 --- /dev/null +++ b/src/main/resources/db/changelog/changesets/changelog_20260116T110615Z.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index e1801e26..2dfde1d1 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -51,3 +51,6 @@ databaseChangeLog: - include: file: changesets/changelog_20250908T143920Z.xml relativeToChangelogFile: true + - include: + file: changesets/changelog_20260116T110615Z.xml + relativeToChangelogFile: true diff --git a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java index 4446d960..0ebb1b22 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/service/SecurityAnalysisResultServiceTest.java @@ -66,12 +66,11 @@ private void checkFieldBasedOnNetworkAreNullish(ContingencyResultDTO contingency .extracting(SubjectLimitViolationDTO::getLimitViolation) .allSatisfy(lv -> assertThat(lv) - .as("limitViolation") .satisfies(v -> assertThat(v.getPatlLimit()).isNull()) .satisfies(v -> assertThat(v.getPatlLoading()).isNull()) .satisfies(v -> assertThat(v.getNextLimitName()).isNull()) .satisfies(v -> assertThat(v.getLocationId()).isNull()) - .satisfies(v -> assertThat(v.getAcceptableDuration()).isEqualTo(Integer.MAX_VALUE)) + .satisfies(v -> assertThat(v.getAcceptableDuration()).isNull()) ); }