diff --git a/src/main/java/org/gridsuite/study/server/controller/StudyController.java b/src/main/java/org/gridsuite/study/server/controller/StudyController.java index a09335b219..8529afc919 100644 --- a/src/main/java/org/gridsuite/study/server/controller/StudyController.java +++ b/src/main/java/org/gridsuite/study/server/controller/StudyController.java @@ -110,14 +110,14 @@ public StudyController(StudyService studyService, @InitBinder public void initBinder(WebDataBinder webdataBinder) { webdataBinder.registerCustomEditor(EquipmentInfosService.FieldSelector.class, - new MyEnumConverter<>(EquipmentInfosService.FieldSelector.class)); + new MyEnumConverter<>(EquipmentInfosService.FieldSelector.class)); webdataBinder.registerCustomEditor(ModificationType.class, new MyModificationTypeConverter()); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/case/name") @Operation(summary = "Get study case name") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The study case name"), - @ApiResponse(responseCode = "204", description = "The study has no case name attached")}) + @ApiResponse(responseCode = "204", description = "The study has no case name attached")}) public ResponseEntity getStudyCaseName(@PathVariable("studyUuid") UUID studyUuid, @PathVariable("rootNetworkUuid") UUID rootNetworkUuid) { String studyCaseName = rootNetworkService.getCaseName(rootNetworkUuid); return StringUtils.isEmpty(studyCaseName) ? ResponseEntity.noContent().build() : ResponseEntity.ok().body(studyCaseName); @@ -160,7 +160,7 @@ public ResponseEntity createStudy(@PathVariable("caseUuid") UUI @ApiResponse(responseCode = "200", description = "The study was successfully created"), @ApiResponse(responseCode = "404", description = "The source study doesn't exist")}) public ResponseEntity duplicateStudy(@RequestParam("duplicateFrom") UUID studyId, - @RequestHeader(HEADER_USER_ID) String userId) { + @RequestHeader(HEADER_USER_ID) String userId) { UUID newStudyId = studyService.duplicateStudy(studyId, userId); return newStudyId != null ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(newStudyId) : ResponseEntity.notFound().build(); @@ -217,7 +217,7 @@ public ResponseEntity updateRootNetwork(@PathVariable("studyUuid") UUID st @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The root network exists"), @ApiResponse(responseCode = "204", description = "The root network doesn't exist")}) public ResponseEntity rootNetworkExists(@PathVariable("studyUuid") UUID studyUuid, - @RequestParam("name") String rootNetworkName) { + @RequestParam("name") String rootNetworkName) { HttpStatus status = rootNetworkService.isRootNetworkNameExistsInStudy(studyUuid, rootNetworkName) ? HttpStatus.OK : HttpStatus.NO_CONTENT; return ResponseEntity.status(status).contentType(MediaType.APPLICATION_JSON).build(); } @@ -227,7 +227,7 @@ public ResponseEntity rootNetworkExists(@PathVariable("studyUuid") UUID st @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The tag exists"), @ApiResponse(responseCode = "204", description = "The tag doesn't exist")}) public ResponseEntity rootNetworkTagExists(@PathVariable("studyUuid") UUID studyUuid, - @RequestParam("tag") String rootNetworkTag) { + @RequestParam("tag") String rootNetworkTag) { HttpStatus status = rootNetworkService.isRootNetworkTagExistsInStudy(studyUuid, rootNetworkTag) ? HttpStatus.OK : HttpStatus.NO_CONTENT; return ResponseEntity.status(status).contentType(MediaType.APPLICATION_JSON).build(); } @@ -236,8 +236,8 @@ public ResponseEntity rootNetworkTagExists(@PathVariable("studyUuid") UUID @Operation(summary = "Delete root networks for study") @ApiResponse(responseCode = "200", description = "Root network deleted") public ResponseEntity deleteRootNetwork(@PathVariable("studyUuid") UUID studyUuid, - @RequestBody List rootNetworksUuids, - @RequestHeader(HEADER_USER_ID) String userId) { + @RequestBody List rootNetworksUuids, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.deleteRootNetworks(studyUuid, rootNetworksUuids, userId); return ResponseEntity.ok().build(); } @@ -253,7 +253,7 @@ public ResponseEntity duplicateNode(@Parameter(description = "The study wh @Parameter(description = "The node we want to copy") @RequestParam("nodeToCopyUuid") UUID nodeToCopyUuid, @Parameter(description = "The reference node to where we want to paste") @RequestParam("referenceNodeUuid") UUID referenceNodeUuid, @Parameter(description = "The position where the node will be pasted relative to the reference node") - @RequestParam(name = "insertMode") InsertMode insertMode, + @RequestParam(name = "insertMode") InsertMode insertMode, @RequestHeader(HEADER_USER_ID) String userId) { //if the source study is not set we assume it's the same as the target study studyService.assertNoBlockedNodeInStudy(targetStudyUuid, referenceNodeUuid); @@ -268,11 +268,11 @@ public ResponseEntity duplicateNode(@Parameter(description = "The study wh @ApiResponse(responseCode = "403", description = "The node can't be copied above the root node nor around itself"), @ApiResponse(responseCode = "404", description = "The source study or node doesn't exist")}) public ResponseEntity cutAndPasteNode(@PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "The node we want to cut") @RequestParam("nodeToCutUuid") UUID nodeToCutUuid, - @Parameter(description = "The reference node to where we want to paste") @RequestParam("referenceNodeUuid") UUID referenceNodeUuid, - @Parameter(description = "The position where the node will be pasted relative to the reference node") - @RequestParam(name = "insertMode") InsertMode insertMode, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "The node we want to cut") @RequestParam("nodeToCutUuid") UUID nodeToCutUuid, + @Parameter(description = "The reference node to where we want to paste") @RequestParam("referenceNodeUuid") UUID referenceNodeUuid, + @Parameter(description = "The position where the node will be pasted relative to the reference node") + @RequestParam(name = "insertMode") InsertMode insertMode, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertNoBlockedNodeInStudy(studyUuid, nodeToCutUuid); studyService.assertCanUpdateNodeInStudy(studyUuid, nodeToCutUuid); studyService.moveStudyNode(studyUuid, nodeToCutUuid, referenceNodeUuid, insertMode, userId); @@ -287,8 +287,8 @@ public ResponseEntity cutAndPasteNode(@PathVariable("studyUuid") UUID stud public ResponseEntity checkNetworkExistence(@PathVariable("studyUuid") UUID studyUuid, @PathVariable("rootNetworkUuid") UUID rootNetworkUuid) { UUID networkUUID = rootNetworkService.getNetworkUuid(rootNetworkUuid); return networkStoreService.doesNetworkExist(networkUUID) - ? ResponseEntity.ok().build() - : ResponseEntity.noContent().build(); + ? ResponseEntity.ok().build() + : ResponseEntity.noContent().build(); } @@ -298,11 +298,11 @@ public ResponseEntity checkNetworkExistence(@PathVariable("studyUuid") UUI @ApiResponse(responseCode = "200", description = "Study network recreation has started"), @ApiResponse(responseCode = "424", description = "The case doesn't exist")}) public ResponseEntity recreateNetworkFromCase(@PathVariable("studyUuid") UUID studyUuid, - @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @RequestBody(required = false) Map importParameters, - @RequestParam(value = "caseUuid") UUID caseUuid, - @Parameter(description = "case format") @RequestParam(name = "caseFormat", required = false) String caseFormat, - @RequestHeader(HEADER_USER_ID) String userId) { + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @RequestBody(required = false) Map importParameters, + @RequestParam(value = "caseUuid") UUID caseUuid, + @Parameter(description = "case format") @RequestParam(name = "caseFormat", required = false) String caseFormat, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.recreateNetwork(caseUuid, userId, studyUuid, rootNetworkUuid, caseFormat, importParameters); return ResponseEntity.ok().build(); } @@ -330,7 +330,7 @@ public ResponseEntity recreateStudyNetwork(@PathVariable("study public ResponseEntity checkRootNetworkIndexationStatus(@PathVariable("studyUuid") UUID studyUuid, @PathVariable("rootNetworkUuid") UUID rootNetworkUuid) { String result = studyService.getRootNetworkIndexationStatus(studyUuid, rootNetworkUuid).name(); return result != null ? ResponseEntity.ok().body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @PostMapping(value = "/studies/{studyUuid}/tree/subtrees", params = {"subtreeToCutParentNodeUuid", "referenceNodeUuid"}) @@ -358,7 +358,7 @@ public ResponseEntity cutAndPasteNodeSubtree(@PathVariable("studyUuid") UU public ResponseEntity duplicateSubtree(@Parameter(description = "The study where we want to copy the node") @PathVariable("studyUuid") UUID targetStudyUuid, @Parameter(description = "The copied node original study") @RequestParam(value = "sourceStudyUuid") UUID sourceStudyUuid, @Parameter(description = "The parent node of the subtree we want to cut") - @RequestParam("subtreeToCopyParentNodeUuid") UUID subtreeToCopyParentNodeUuid, + @RequestParam("subtreeToCopyParentNodeUuid") UUID subtreeToCopyParentNodeUuid, @Parameter(description = "The reference node to where we want to paste") @RequestParam("referenceNodeUuid") UUID referenceNodeUuid, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertNoBlockedNodeInStudy(targetStudyUuid, referenceNodeUuid); @@ -382,7 +382,7 @@ public ResponseEntity generateVoltageLevelDiagram( rootNetworkUuid, sldRequestInfos); return result != null ? ResponseEntity.ok().contentType(MediaType.APPLICATION_XML).body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/voltage-levels/{voltageLevelId}/svg-and-metadata") @@ -401,7 +401,7 @@ public ResponseEntity generateVoltageLevelDiagramAndMetadata( rootNetworkUuid, sldRequestInfos); return result != null ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/voltage-levels/{voltageLevelId}/buses-or-busbar-sections") @@ -415,7 +415,7 @@ public ResponseEntity> getVoltageLevelBusesOrBusbarSecti @Parameter(description = "Should get in upstream built node ?") @RequestParam(value = "inUpstreamBuiltParentNode", required = false, defaultValue = "false") boolean inUpstreamBuiltParentNode) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getVoltageLevelBusesOrBusbarSections(nodeUuid, rootNetworkUuid, voltageLevelId, - inUpstreamBuiltParentNode)); + inUpstreamBuiltParentNode)); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/voltage-levels/{voltageLevelId}/switches") @@ -429,7 +429,7 @@ public ResponseEntity getVoltageLevelSwitches( @Parameter(description = "Should get in upstream built node ?") @RequestParam(value = "inUpstreamBuiltParentNode", required = false, defaultValue = "false") boolean inUpstreamBuiltParentNode) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getVoltageLevelTopologyInfos(nodeUuid, rootNetworkUuid, voltageLevelId, inUpstreamBuiltParentNode, - "switches")); + "switches")); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/voltage-levels/{voltageLevelId}/bus-bar-sections") @@ -443,7 +443,7 @@ public ResponseEntity getVoltageLevelBusBarSections( @Parameter(description = "Should get in upstream built node ?") @RequestParam(value = "inUpstreamBuiltParentNode", required = false, defaultValue = "false") boolean inUpstreamBuiltParentNode) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getVoltageLevelTopologyInfos(nodeUuid, rootNetworkUuid, voltageLevelId, inUpstreamBuiltParentNode, - "bus-bar-sections")); + "bus-bar-sections")); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/voltage-levels/{voltageLevelId}/feeder-bays") @@ -457,7 +457,7 @@ public ResponseEntity getVoltageLevelFeederBays( @Parameter(description = "Should get in upstream built node ?") @RequestParam(value = "inUpstreamBuiltParentNode", required = false, defaultValue = "false") boolean inUpstreamBuiltParentNode) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getVoltageLevelTopologyInfos(nodeUuid, rootNetworkUuid, voltageLevelId, inUpstreamBuiltParentNode, - "feeder-bays")); + "feeder-bays")); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/voltage-levels/{voltageLevelId}/substation-id") @@ -509,7 +509,7 @@ public ResponseEntity getSubstationGraphics( @PathVariable("nodeUuid") UUID nodeUuid, @RequestBody(required = false) List substationsIds) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getSubstationsGraphics(rootNetworkService.getNetworkUuid(rootNetworkUuid), nodeUuid, rootNetworkUuid, - substationsIds)); + substationsIds)); } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network-map/equipments-ids") @@ -526,7 +526,7 @@ public ResponseEntity getNetworkElementsIds( @Parameter(description = "Nominal Voltages") @RequestParam(name = "nominalVoltages", required = false) List nominalVoltages) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getNetworkElementsIds(nodeUuid, rootNetworkUuid, substationsIds, inUpstreamBuiltParentNode, equipmentType, - nominalVoltages)); + nominalVoltages)); } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/elements") @@ -544,7 +544,7 @@ public ResponseEntity getNetworkElementsInfos( @Parameter(description = "Nominal Voltages") @RequestParam(name = "nominalVoltages", required = false) List nominalVoltages) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getNetworkElementsInfos(studyUuid, nodeUuid, rootNetworkUuid, substationsIds, infoType, elementType, - inUpstreamBuiltParentNode, nominalVoltages)); + inUpstreamBuiltParentNode, nominalVoltages)); } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network/elements-by-global-filter") @@ -578,7 +578,7 @@ public ResponseEntity getNetworkElementInfos( defaultValue = "false") boolean inUpstreamBuiltParentNode, @Parameter(description = "Info type parameters") InfoTypeParameters infoTypeParameters) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getNetworkElementInfos(studyUuid, nodeUuid, rootNetworkUuid, elementType, infoTypeParameters, elementId, - inUpstreamBuiltParentNode)); + inUpstreamBuiltParentNode)); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network-map/countries") @@ -652,10 +652,10 @@ public ResponseEntity getAllMapData( @Operation(summary = "move network modification before another") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The modification order is updated")}) public ResponseEntity moveModification(@PathVariable("studyUuid") UUID studyUuid, - @PathVariable("nodeUuid") UUID nodeUuid, - @PathVariable("modificationUuid") UUID modificationUuid, - @Nullable @Parameter(description = "move before, if no value move to end") @RequestParam(value = "beforeUuid") UUID beforeUuid, - @RequestHeader(HEADER_USER_ID) String userId) { + @PathVariable("nodeUuid") UUID nodeUuid, + @PathVariable("modificationUuid") UUID modificationUuid, + @Nullable @Parameter(description = "move before, if no value move to end") @RequestParam(value = "beforeUuid") UUID beforeUuid, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertCanUpdateNodeInStudy(studyUuid, nodeUuid); studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid); rebuildNodeService.moveNetworkModification(studyUuid, nodeUuid, modificationUuid, beforeUuid, userId); @@ -686,12 +686,12 @@ public ResponseEntity moveSubModification( @Operation(summary = "For a list of network modifications passed in body, copy or cut, then append them to target node") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The modification list has been updated.")}) public ResponseEntity moveOrCopyModifications(@PathVariable("studyUuid") UUID studyUuid, - @PathVariable("nodeUuid") UUID nodeUuid, - @RequestParam("action") ModificationsActionType action, - @RequestParam("originStudyUuid") UUID originStudyUuid, - @RequestParam("originNodeUuid") UUID originNodeUuid, - @RequestBody List modificationsToCopyUuidList, - @RequestHeader(HEADER_USER_ID) String userId) { + @PathVariable("nodeUuid") UUID nodeUuid, + @RequestParam("action") ModificationsActionType action, + @RequestParam("originStudyUuid") UUID originStudyUuid, + @RequestParam("originNodeUuid") UUID originNodeUuid, + @RequestBody List modificationsToCopyUuidList, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); studyService.assertIsStudyAndNodeExist(originStudyUuid, originNodeUuid); studyService.assertCanUpdateNodeInStudy(studyUuid, nodeUuid); @@ -734,10 +734,10 @@ public ResponseEntity assembleModificationsIntoNewComposite( @Operation(summary = "For a list of composite network modifications passed in body, insert them into the target node") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification list has been inserted.")}) public ResponseEntity insertCompositeModifications(@PathVariable("studyUuid") UUID studyUuid, - @PathVariable("nodeUuid") UUID nodeUuid, - @RequestParam("action") CompositeModificationsActionType action, - @RequestBody List> modificationsToInsert, - @RequestHeader(HEADER_USER_ID) String userId) { + @PathVariable("nodeUuid") UUID nodeUuid, + @RequestParam("action") CompositeModificationsActionType action, + @RequestBody List> modificationsToInsert, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); studyService.assertCanUpdateNodeInStudy(studyUuid, nodeUuid); handleInsertCompositeNetworkModifications(studyUuid, nodeUuid, modificationsToInsert, userId, action); @@ -745,7 +745,7 @@ public ResponseEntity insertCompositeModifications(@PathVariable("studyUui } private void handleInsertCompositeNetworkModifications(UUID targetStudyUuid, UUID targetNodeUuid, List> modificationsToCopy, String userId, - CompositeModificationsActionType action) { + CompositeModificationsActionType action) { studyService.assertNoBlockedNodeInStudy(targetStudyUuid, targetNodeUuid); studyService.invalidateNodeTreeWithLF(targetStudyUuid, targetNodeUuid); try { @@ -775,6 +775,7 @@ public ResponseEntity runLoadFlow( @RequestParam(value = "withRatioTapChangers", required = false, defaultValue = "false") boolean withRatioTapChangers, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(LOAD_FLOW, userId); studyService.assertNoBlockedNodeInTree(nodeUuid, rootNetworkUuid); studyService.assertCanRunOnConstructionNode(studyUuid, nodeUuid, List.of(DYNA_FLOW_PROVIDER), studyService::getLoadFlowProvider); UUID prevResultUuid = rootNetworkNodeInfoService.getComputationResultUuid(nodeUuid, rootNetworkUuid, LOAD_FLOW); @@ -825,8 +826,8 @@ public ResponseEntity getLoadflowResult(@Parameter(description = "study @ApiResponse(responseCode = "204", description = "No loadflow has been done yet"), @ApiResponse(responseCode = "404", description = "The loadflow status has not been found")}) public ResponseEntity getLoadFlowStatus(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { String result = rootNetworkNodeInfoService.getLoadFlowStatus(nodeUuid, rootNetworkUuid); return result != null ? ResponseEntity.ok().body(result) : ResponseEntity.noContent().build(); } @@ -846,8 +847,8 @@ public ResponseEntity getLoadFlowComputationInfos(@Par @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The loadflow computation infos"), @ApiResponse(responseCode = "404", description = "The loadflow computation has not been found")}) public ResponseEntity getLoadFlowModifications(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(rootNetworkNodeInfoService.getLoadFlowModifications(nodeUuid, rootNetworkUuid)); } @@ -873,6 +874,7 @@ public ResponseEntity runShortCircuit( @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(SHORT_CIRCUIT, userId); studyService.runShortCircuit(studyUuid, nodeUuid, rootNetworkUuid, busId, debug, userId); return ResponseEntity.ok().build(); } @@ -897,9 +899,9 @@ public ResponseEntity getShortCircuitResult(@Parameter(description = "st @Parameter(description = "root network UUID") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, @Parameter(description = "node UUID") @PathVariable("nodeUuid") UUID nodeUuid, @Parameter(description = "BASIC (faults without limits and feeders), " + - "FULL (faults with both), " + - "WITH_LIMIT_VIOLATIONS (like FULL but only those with limit violations) or " + - "NONE (no fault)") @RequestParam(name = "mode", required = false, defaultValue = "FULL") FaultResultsMode mode, + "FULL (faults with both), " + + "WITH_LIMIT_VIOLATIONS (like FULL but only those with limit violations) or " + + "NONE (no fault)") @RequestParam(name = "mode", required = false, defaultValue = "FULL") FaultResultsMode mode, @Parameter(description = "type") @RequestParam(value = "type", required = false, defaultValue = "ALL_BUSES") ShortcircuitAnalysisType type, @Parameter(description = "JSON array of filters") @RequestParam(name = "filters", required = false) String filters, @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, @@ -949,21 +951,21 @@ public ResponseEntity getShortCircuitAnalysisCsvResult( @ApiResponse(responseCode = "204", description = "No pcc min has been done yet"), @ApiResponse(responseCode = "404", description = "The pcc min has not been found")}) public ResponseEntity exportPccMinResultsAsCsv( - @Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "JSON array of filters") @RequestParam(name = "filters", required = false) String filters, - @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, - Sort sort, @RequestBody String csvHeaders) { + @Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "JSON array of filters") @RequestParam(name = "filters", required = false) String filters, + @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, + Sort sort, @RequestBody String csvHeaders) { byte[] result = rootNetworkNodeInfoService.exportPccMinResultsAsCsv(nodeUuid, rootNetworkUuid, csvHeaders, sort, filters, globalFilters); HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); responseHeaders.setContentDispositionFormData("attachment", "pcc_min_results.csv"); return ResponseEntity - .ok() - .headers(responseHeaders) - .body(result); + .ok() + .headers(responseHeaders) + .body(result); } @PutMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/voltage-init/run") @@ -977,6 +979,7 @@ public ResponseEntity runVoltageInit( @Parameter(description = "debug") @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(VOLTAGE_INITIALIZATION, userId); studyService.runVoltageInit(studyUuid, nodeUuid, rootNetworkUuid, userId, debug); return ResponseEntity.ok().build(); } @@ -998,9 +1001,9 @@ public ResponseEntity stopVoltageInit(@Parameter(description = "Study uuid @ApiResponse(responseCode = "204", description = "No voltage init has been done yet"), @ApiResponse(responseCode = "404", description = "The voltage init has not been found")}) public ResponseEntity getVoltageInitResult(@Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters) { String result = studyService.getVoltageInitResult(nodeUuid, rootNetworkUuid, globalFilters); return result != null ? ResponseEntity.ok().body(result) : ResponseEntity.noContent().build(); @@ -1063,12 +1066,12 @@ public ResponseEntity exportNetwork( studyService.assertRootNodeOrBuiltNode(studyUuid, nodeUuid, rootNetworkUuid); UUID parentDirectoryUuid = parentDirectoryUuidStr != null ? UUID.fromString(parentDirectoryUuidStr) : null; UUID exportUuid = studyService.exportNetwork(studyUuid, - nodeUuid, - rootNetworkUuid, - new NodeExportInfos(exportToGridExplore, parentDirectoryUuid, fileName, description), - format, - userId, - parametersJson); + nodeUuid, + rootNetworkUuid, + new NodeExportInfos(exportToGridExplore, parentDirectoryUuid, fileName, description), + format, + userId, + parametersJson); return ResponseEntity.ok().body(exportUuid); } @@ -1092,10 +1095,11 @@ public ResponseEntity downloadExportedNetworkFile( @Operation(summary = "run security analysis on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis has started")}) public ResponseEntity runSecurityAnalysis(@Parameter(description = "studyUuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(SECURITY_ANALYSIS, userId); studyService.runSecurityAnalysis(studyUuid, nodeUuid, rootNetworkUuid, userId); return ResponseEntity.ok().build(); } @@ -1106,15 +1110,15 @@ public ResponseEntity runSecurityAnalysis(@Parameter(description = "studyU @ApiResponse(responseCode = "204", description = "No security analysis has been done yet"), @ApiResponse(responseCode = "404", description = "The security analysis has not been found")}) public ResponseEntity getSecurityAnalysisResult(@Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "result type") @RequestParam(name = "resultType") SecurityAnalysisResultType resultType, - @Parameter(description = "JSON array of filters") @RequestParam(name = "filters", required = false) String filters, - @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, - Pageable pageable) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "result type") @RequestParam(name = "resultType") SecurityAnalysisResultType resultType, + @Parameter(description = "JSON array of filters") @RequestParam(name = "filters", required = false) String filters, + @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, + Pageable pageable) { String result = rootNetworkNodeInfoService.getSecurityAnalysisResult(nodeUuid, rootNetworkUuid, resultType, filters, globalFilters, pageable); return result != null ? ResponseEntity.ok().body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/security-analysis/result/csv") @@ -1142,18 +1146,19 @@ public ResponseEntity getContingencyCount(@Parameter(descripti @Parameter(description = "Contingency list UUIDs") @RequestParam(name = "contingencyListIds", required = false) List contingencyListIds) { return ResponseEntity.ok().body(CollectionUtils.isEmpty(contingencyListIds) ? ActionsService.EMPTY_CONTINGENCY_COUNT : studyService.getContingencyCount(studyUuid, contingencyListIds, nodeUuid, - rootNetworkUuid)); + rootNetworkUuid)); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/limit-violations") @Operation(summary = "Get limit violations.") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The limit violations")}) - public ResponseEntity> getLimitViolations(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "JSON array of filters") @RequestParam(name = "filters", required = false) String filters, - @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, - Sort sort) { + public ResponseEntity> getLimitViolations( + @Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "JSON array of filters") @RequestParam(name = "filters", required = false) String filters, + @Parameter(description = "JSON array of global filters") @RequestParam(name = "globalFilters", required = false) String globalFilters, + Sort sort) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getLimitViolations(nodeUuid, rootNetworkUuid, filters, globalFilters, sort)); } @@ -1161,17 +1166,17 @@ public ResponseEntity> getLimitViolations(@Parameter(d @Operation(summary = "Get Enum values") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The Enum values")}) public ResponseEntity> getResultEnumValues(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "Computing Type") @RequestParam(name = "computingType") ComputationType computingType, - @Parameter(description = "Enum name") @RequestParam(name = "enumName") String enumName) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "Computing Type") @RequestParam(name = "computingType") ComputationType computingType, + @Parameter(description = "Enum name") @RequestParam(name = "enumName") String enumName) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getResultEnumValues(nodeUuid, rootNetworkUuid, computingType, enumName)); } @PostMapping(value = "/studies/{studyUuid}/loadflow/parameters") @Operation(summary = "set loadflow parameters on study, reset to default ones if empty body") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The loadflow parameters are set"), - @ApiResponse(responseCode = "204", description = "Reset with user profile cannot be done")}) + @ApiResponse(responseCode = "204", description = "Reset with user profile cannot be done")}) public ResponseEntity setLoadflowParameters( @PathVariable("studyUuid") UUID studyUuid, @RequestBody(required = false) String lfParameter, @@ -1265,7 +1270,7 @@ public ResponseEntity generateSubstationDiagramAndMetadata( @RequestBody Map sldRequestInfos) { String result = studyService.generateSubstationSvgAndMetadata(substationId, nodeUuid, rootNetworkUuid, sldRequestInfos); return result != null ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network-area-diagram", produces = MediaType.APPLICATION_JSON_VALUE) @@ -1278,7 +1283,7 @@ public ResponseEntity generateNetworkAreaDiagram( @RequestBody Map nadRequestInfos) { String result = studyService.generateNetworkAreaDiagram(nodeUuid, rootNetworkUuid, nadRequestInfos); return result != null ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/security-analysis/status") @@ -1287,8 +1292,8 @@ public ResponseEntity generateNetworkAreaDiagram( @ApiResponse(responseCode = "204", description = "No security analysis has been done yet"), @ApiResponse(responseCode = "404", description = "The security analysis status has not been found")}) public ResponseEntity getSecurityAnalysisStatus(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { String status = rootNetworkNodeInfoService.getSecurityAnalysisStatus(nodeUuid, rootNetworkUuid); return status != null ? ResponseEntity.ok().body(status) : ResponseEntity.noContent().build(); } @@ -1308,12 +1313,12 @@ public ResponseEntity stopSecurityAnalysis(@Parameter(description = "Study @Operation(summary = "Get node report with its parent nodes") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The node report"), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity> getParentNodesReport(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "Root network uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "Node uuid") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "Node only report") @RequestParam(value = "nodeOnlyReport", required = false, - defaultValue = "true") boolean nodeOnlyReport, - @Parameter(description = "The report Type") @RequestParam(name = "reportType") StudyService.ReportType reportType, - @Parameter(description = "Severity levels") @RequestParam(name = "severityLevels", required = false) Set severityLevels) { + @Parameter(description = "Root network uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "Node uuid") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "Node only report") @RequestParam(value = "nodeOnlyReport", required = false, + defaultValue = "true") boolean nodeOnlyReport, + @Parameter(description = "The report Type") @RequestParam(name = "reportType") StudyService.ReportType reportType, + @Parameter(description = "Severity levels") @RequestParam(name = "severityLevels", required = false) Set severityLevels) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getParentNodesReport(nodeUuid, rootNetworkUuid, nodeOnlyReport, reportType, severityLevels)); } @@ -1323,15 +1328,15 @@ public ResponseEntity> getParentNodesReport(@Parameter(description @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The report logs of the node and all its parent"), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity getReportLogs(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "node id") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "report id") @RequestParam(name = "reportId", required = false) UUID reportId, - @Parameter(description = "The message filter") @RequestParam(name = "message", required = false) String messageFilter, - @Parameter(description = "Severity levels filter") @RequestParam(name = "severityLevels", - required = false) Set severityLevels, - @Parameter(description = "If we wanted the paged version of the results or not") @RequestParam(name = "paged", required = false, - defaultValue = "false") boolean paged, - Pageable pageable) { + @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "node id") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "report id") @RequestParam(name = "reportId", required = false) UUID reportId, + @Parameter(description = "The message filter") @RequestParam(name = "message", required = false) String messageFilter, + @Parameter(description = "Severity levels filter") @RequestParam(name = "severityLevels", + required = false) Set severityLevels, + @Parameter(description = "If we wanted the paged version of the results or not") @RequestParam(name = "paged", required = false, + defaultValue = "false") boolean paged, + Pageable pageable) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); rootNetworkService.assertIsRootNetworkInStudy(studyUuid, rootNetworkUuid); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getReportLogs(nodeUuid, rootNetworkUuid, reportId, messageFilter, severityLevels, paged, pageable)); @@ -1364,9 +1369,9 @@ public ResponseEntity getSearchTermMatchesInFilteredLogs( @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The report severities of the node and all its parent"), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity> getParentNodesAggregatedReportSeverities(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "node id") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "reportId") @RequestParam(name = "reportId", required = false) UUID reportId) { + @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "node id") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "reportId") @RequestParam(name = "reportId", required = false) UUID reportId) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); rootNetworkService.assertIsRootNetworkInStudy(studyUuid, rootNetworkUuid); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getAggregatedReportSeverities(nodeUuid, rootNetworkUuid, reportId)); @@ -1384,10 +1389,10 @@ public ResponseEntity> getAvailableSvgComponentLibraries() { @Operation(summary = "Get network modifications from a node") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The network modifications was returned"), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity getNetworkModifications(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, - @RequestParam(name = "onlyStashed", required = false, defaultValue = "false") Boolean onlyStashed, - @Parameter(description = "Only metadata") @RequestParam(name = "onlyMetadata", required = false, - defaultValue = "false") Boolean onlyMetadata) { + @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, + @RequestParam(name = "onlyStashed", required = false, defaultValue = "false") Boolean onlyStashed, + @Parameter(description = "Only metadata") @RequestParam(name = "onlyMetadata", required = false, + defaultValue = "false") Boolean onlyMetadata) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(networkModificationTreeService.getNetworkModifications(nodeUuid, onlyStashed, onlyMetadata)); } @@ -1397,7 +1402,7 @@ public ResponseEntity getNetworkModifications(@Parameter(description = " @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The network modifications to export was returned"), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity getExportedNetworkModifications(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getExportedNetworkModifications(studyUuid, nodeUuid)); } @@ -1407,7 +1412,7 @@ public ResponseEntity getExportedNetworkModifications(@Parameter(descrip @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The excluded network modifications were returned"), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity> getNetworkModificationsToExclude(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid) { studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(networkModificationTreeService.getModificationsToExclude(nodeUuid)); @@ -1458,10 +1463,10 @@ public ResponseEntity deleteNetworkModifications(@Parameter(description = @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The network modifications were stashed / restored "), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity stashNetworkModifications(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "Network modification UUIDs") @RequestParam("uuids") List networkModificationUuids, - @Parameter(description = "Stashed Modification") @RequestParam(name = "stashed", required = true) Boolean stashed, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "Network modification UUIDs") @RequestParam("uuids") List networkModificationUuids, + @Parameter(description = "Stashed Modification") @RequestParam(name = "stashed", required = true) Boolean stashed, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertCanUpdateNodeInStudy(studyUuid, nodeUuid); studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid); if (stashed.booleanValue()) { @@ -1526,7 +1531,7 @@ public ResponseEntity> searchEquipments( @Parameter(description = "Should search in upstream built node") @RequestParam(value = "inUpstreamBuiltParentNode", required = false, defaultValue = "false") boolean inUpstreamBuiltParentNode, @Parameter(description = "Equipment type") @RequestParam(value = "equipmentType", required = false) String equipmentType) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON) - .body(studyService.searchEquipments(nodeUuid, rootNetworkUuid, userInput, fieldSelector, equipmentType, inUpstreamBuiltParentNode)); + .body(studyService.searchEquipments(nodeUuid, rootNetworkUuid, userInput, fieldSelector, equipmentType, inUpstreamBuiltParentNode)); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/modifications/indexation-infos", produces = MediaType.APPLICATION_JSON_VALUE) @@ -1536,9 +1541,9 @@ public ResponseEntity> searchEquipments( @ApiResponse(responseCode = "404", description = "The study not found"), }) public ResponseEntity> searchModifications( - @Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "Root network uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "User input") @RequestParam(value = "userInput") String userInput) { + @Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "Root network uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "User input") @RequestParam(value = "userInput") String userInput) { studyService.assertIsStudyExist(studyUuid); rootNetworkService.assertIsRootNetworkInStudy(studyUuid, rootNetworkUuid); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON) @@ -1551,11 +1556,11 @@ public ResponseEntity> searchModifications @ApiResponse(responseCode = "200", description = "The node has been added"), @ApiResponse(responseCode = "404", description = "The study or the node not found")}) public ResponseEntity createNode(@RequestBody NetworkModificationNode node, - @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "parent id of the node created") @PathVariable(name = "id") UUID referenceId, - @Parameter(description = "node is inserted before the given node ID") @RequestParam(name = "mode", required = false, - defaultValue = "CHILD") InsertMode insertMode, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "parent id of the node created") @PathVariable(name = "id") UUID referenceId, + @Parameter(description = "node is inserted before the given node ID") @RequestParam(name = "mode", required = false, + defaultValue = "CHILD") InsertMode insertMode, + @RequestHeader(HEADER_USER_ID) String userId) { NetworkModificationNode newNode = studyService.createNode(studyUuid, referenceId, node, insertMode, userId); studyService.createNodePostAction(studyUuid, referenceId, newNode, userId); @@ -1569,10 +1574,10 @@ public ResponseEntity createNode(@RequestBody NetworkMo @ApiResponse(responseCode = "200", description = "The node sequence has been added"), @ApiResponse(responseCode = "404", description = "The study or the node not found")}) public ResponseEntity createSequence( - @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "parent id of the node created") @PathVariable(name = "id") UUID referenceId, - @Parameter(description = "sequence to create") @RequestParam("sequenceType") NodeSequenceType nodeSequenceType, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "parent id of the node created") @PathVariable(name = "id") UUID referenceId, + @Parameter(description = "sequence to create") @RequestParam("sequenceType") NodeSequenceType nodeSequenceType, + @RequestHeader(HEADER_USER_ID) String userId) { NetworkModificationNode sequenceParentNode = studyService.createSequence(studyUuid, referenceId, nodeSequenceType, userId); studyService.createSequencePostAction(studyUuid, sequenceParentNode.getId(), nodeSequenceType, userId); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(sequenceParentNode); @@ -1584,9 +1589,9 @@ public ResponseEntity createSequence( @ApiResponse(responseCode = "200", description = "Nodes have been successfully deleted"), @ApiResponse(responseCode = "404", description = "The study or the nodes not found")}) public ResponseEntity deleteNodes(@Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "ids of children to remove") @RequestParam("ids") List nodeIds, - @Parameter(description = "deleteChildren") @RequestParam(value = "deleteChildren", defaultValue = "false") boolean deleteChildren, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "ids of children to remove") @RequestParam("ids") List nodeIds, + @Parameter(description = "deleteChildren") @RequestParam(value = "deleteChildren", defaultValue = "false") boolean deleteChildren, + @RequestHeader(HEADER_USER_ID) String userId) { nodeIds.stream().forEach(nodeId -> studyService.assertNoBlockedNodeInStudy(studyUuid, nodeId)); studyService.deleteNodes(studyUuid, nodeIds, deleteChildren, userId); return ResponseEntity.ok().build(); @@ -1599,9 +1604,9 @@ public ResponseEntity deleteNodes(@Parameter(description = "study uuid") @ @ApiResponse(responseCode = "403", description = "The node can't be moved to trash"), @ApiResponse(responseCode = "404", description = "The study or the node not found")}) public ResponseEntity stashNode(@Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "id of child to delete (move to trash)") @PathVariable("id") UUID nodeId, - @Parameter(description = "to stash a node with its children") @RequestParam(value = "stashChildren", defaultValue = "false") boolean stashChildren, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "id of child to delete (move to trash)") @PathVariable("id") UUID nodeId, + @Parameter(description = "to stash a node with its children") @RequestParam(value = "stashChildren", defaultValue = "false") boolean stashChildren, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertNoBlockedNodeInStudy(studyUuid, nodeId); studyService.assertCanUpdateNodeInStudy(studyUuid, nodeId); studyService.stashNode(studyUuid, nodeId, stashChildren, userId); @@ -1621,9 +1626,9 @@ public ResponseEntity>> getStashedNodes(@Parame @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "The list of nodes in the trash")}) public ResponseEntity restoreNodes(@Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "ids of nodes to restore") @RequestParam("ids") List nodeIds, - @Parameter(description = "id of node below which the node will be restored") @RequestParam("anchorNodeId") UUID anchorNodeId, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "ids of nodes to restore") @RequestParam("ids") List nodeIds, + @Parameter(description = "id of node below which the node will be restored") @RequestParam("anchorNodeId") UUID anchorNodeId, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.restoreNodes(studyUuid, nodeIds, anchorNodeId, userId); return ResponseEntity.ok().build(); } @@ -1637,8 +1642,8 @@ public ResponseEntity getNetworkModificationTree(@Parameter(descriptio @Parameter(description = "root network uuid") @RequestParam(value = "rootNetworkUuid", required = false) UUID rootNetworkUuid) { RootNode rootNode = networkModificationTreeService.getStudyTree(studyUuid, rootNetworkUuid); return rootNode != null ? - ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(rootNode) - : ResponseEntity.notFound().build(); + ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(rootNode) + : ResponseEntity.notFound().build(); } @GetMapping(value = "/studies/{studyUuid}/subtree") @@ -1646,9 +1651,10 @@ public ResponseEntity getNetworkModificationTree(@Parameter(descriptio @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "network modification subtree"), @ApiResponse(responseCode = "404", description = "The study or the parent node not found")}) - public ResponseEntity getNetworkModificationSubtree(@Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "parent node uuid") @RequestParam(value = "parentNodeUuid") UUID parentNodeUuid, - @Parameter(description = "root network uuid") @RequestParam(value = "rootNetworkUuid", required = false) UUID rootNetworkUuid) { + public ResponseEntity getNetworkModificationSubtree( + @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "parent node uuid") @RequestParam(value = "parentNodeUuid") UUID parentNodeUuid, + @Parameter(description = "root network uuid") @RequestParam(value = "rootNetworkUuid", required = false) UUID rootNetworkUuid) { NetworkModificationNode parentNode = (NetworkModificationNode) networkModificationTreeService.getStudySubtree(studyUuid, parentNodeUuid, rootNetworkUuid); return parentNode != null ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(parentNode) @@ -1661,8 +1667,8 @@ public ResponseEntity getNetworkModificationSubtree(@Pa @ApiResponse(responseCode = "200", description = "The node has been updated"), @ApiResponse(responseCode = "404", description = "The study or the node not found")}) public ResponseEntity updateNode(@RequestBody NetworkModificationNode node, - @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, + @RequestHeader(HEADER_USER_ID) String userId) { networkModificationTreeService.updateNode(studyUuid, node, userId); return ResponseEntity.ok().build(); } @@ -1673,9 +1679,9 @@ public ResponseEntity updateNode(@RequestBody NetworkModificationNode node @ApiResponse(responseCode = "200", description = "The node column positions have been updated"), @ApiResponse(responseCode = "404", description = "The study or a node was not found")}) public ResponseEntity updateNodesColumnPositions(@RequestBody List children, - @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "parent node uuid") @PathVariable("parentUuid") UUID parentUuid, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "parent node uuid") @PathVariable("parentUuid") UUID parentUuid, + @RequestHeader(HEADER_USER_ID) String userId) { networkModificationTreeService.updateNodesColumnPositions(studyUuid, parentUuid, children, userId); return ResponseEntity.ok().build(); } @@ -1718,8 +1724,8 @@ public ResponseEntity getUniqueNodeName(@Parameter(description = "Study @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/build") @Operation(summary = "build a study node") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The study node has been built"), - @ApiResponse(responseCode = "404", description = "The study or node doesn't exist"), - @ApiResponse(responseCode = "403", description = "The study node can't be built")}) + @ApiResponse(responseCode = "404", description = "The study or node doesn't exist"), + @ApiResponse(responseCode = "403", description = "The study node can't be built")}) public ResponseEntity buildNode(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, @@ -1736,9 +1742,9 @@ public ResponseEntity buildNode(@Parameter(description = "Study uuid") @Pa @ApiResponse(responseCode = "404", description = "The study or node doesn't exist"), @ApiResponse(responseCode = "403", description = "The study node is not a model node")}) public ResponseEntity unbuildNode(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertNoBlockedNodeInTree(nodeUuid, rootNetworkUuid); studyService.unbuildStudyNode(studyUuid, nodeUuid, rootNetworkUuid, userId); return ResponseEntity.ok().build(); @@ -1763,10 +1769,10 @@ public ResponseEntity unbuildAllNodes(@Parameter(description = "Study uuid @PutMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/build/stop") @Operation(summary = "stop a node build") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The build has been stopped"), - @ApiResponse(responseCode = "404", description = "The study or node doesn't exist")}) + @ApiResponse(responseCode = "404", description = "The study or node doesn't exist")}) public ResponseEntity stopBuild(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "rootNetworkUuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { studyService.stopBuild(nodeUuid, rootNetworkUuid); return ResponseEntity.ok().build(); } @@ -1775,7 +1781,7 @@ public ResponseEntity stopBuild(@Parameter(description = "Study uuid") @Pa @Operation(summary = "reindex root network") @ApiResponse(responseCode = "200", description = "Root network reindexed") public ResponseEntity reindexRootNetwork(@Parameter(description = "study uuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "root network uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid) { + @Parameter(description = "root network uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid) { studyService.reindexRootNetwork(studyUuid, rootNetworkUuid); return ResponseEntity.ok().build(); } @@ -1799,6 +1805,7 @@ public ResponseEntity runSensitivityAnalysis(@Parameter(description = "stu @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(SENSITIVITY_ANALYSIS, userId); studyService.runSensitivityAnalysis(studyUuid, nodeUuid, rootNetworkUuid, userId); return ResponseEntity.ok().build(); } @@ -1818,7 +1825,7 @@ public ResponseEntity getSensitivityAnalysisResult( ) { String result = rootNetworkNodeInfoService.getSensitivityAnalysisResult(nodeUuid, rootNetworkUuid, selector, filters, globalFilters); return result != null ? ResponseEntity.ok().body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/sensitivity-analysis/result/csv", consumes = MediaType.APPLICATION_JSON_VALUE) @@ -1857,7 +1864,7 @@ public ResponseEntity getSensitivityAnalysisFilterOptions( @Parameter(description = "results selector") @RequestParam("selector") String selector) { String result = rootNetworkNodeInfoService.getSensitivityResultsFilterOptions(nodeUuid, rootNetworkUuid, selector); return result != null ? ResponseEntity.ok().body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/sensitivity-analysis/status") @@ -1870,7 +1877,7 @@ public ResponseEntity getSensitivityAnalysisStatus(@Parameter(descriptio @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { String result = rootNetworkNodeInfoService.getSensitivityAnalysisStatus(nodeUuid, rootNetworkUuid); return result != null ? ResponseEntity.ok().body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @PutMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/sensitivity-analysis/stop") @@ -1922,8 +1929,8 @@ public ResponseEntity> getDynamicSimulationEvents(@Parameter(de @ApiResponse(responseCode = "200", description = "The dynamic simulation event was returned"), @ApiResponse(responseCode = "404", description = "The study/node is not found")}) public ResponseEntity getDynamicSimulationEvent(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "Equipment id") @RequestParam(value = "equipmentId") String equipmentId) { + @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "Equipment id") @RequestParam(value = "equipmentId") String equipmentId) { EventInfos dynamicSimulationEvent = studyService.getDynamicSimulationEvent(nodeUuid, equipmentId); return dynamicSimulationEvent != null ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(dynamicSimulationEvent) : ResponseEntity.noContent().build(); @@ -1980,6 +1987,7 @@ public ResponseEntity runDynamicSimulation(@Parameter(description = "study @Parameter(description = "debug") @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(DYNAMIC_SIMULATION, userId); studyService.assertCanRunOnConstructionNode(studyUuid, nodeUuid, List.of(DYNAWO_PROVIDER), studyService::getDynamicSimulationProvider); studyService.runDynamicSimulation(studyUuid, nodeUuid, rootNetworkUuid, userId, debug); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).build(); @@ -2066,11 +2074,12 @@ public ResponseEntity getDynamicSecurityAnalysisParameters( @Operation(summary = "run dynamic security analysis on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The dynamic security analysis has started")}) public ResponseEntity runDynamicSecurityAnalysis(@Parameter(description = "studyUuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "debug") @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, - @RequestHeader(HEADER_USER_ID) String userId) { + @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "debug") @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(DYNAMIC_SECURITY_ANALYSIS, userId); studyService.assertCanRunOnConstructionNode(studyUuid, nodeUuid, List.of(DYNAWO_PROVIDER), studyService::getDynamicSecurityAnalysisProvider); studyService.runDynamicSecurityAnalysis(studyUuid, nodeUuid, rootNetworkUuid, userId, debug); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).build(); @@ -2082,8 +2091,8 @@ public ResponseEntity runDynamicSecurityAnalysis(@Parameter(description = @ApiResponse(responseCode = "204", description = "No dynamic security analysis status"), @ApiResponse(responseCode = "404", description = "The dynamic security analysis has not been found")}) public ResponseEntity getDynamicSecurityAnalysisStatus(@Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { String result = rootNetworkNodeInfoService.getDynamicSecurityAnalysisStatus(nodeUuid, rootNetworkUuid); return result != null ? ResponseEntity.ok().body(result) : ResponseEntity.noContent().build(); } @@ -2118,11 +2127,12 @@ public ResponseEntity getDynamicMarginCalculationParameters( @Operation(summary = "run dynamic margin calculation on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The dynamic margin calculation has started")}) public ResponseEntity runDynamicMarginCalculation(@Parameter(description = "studyUuid") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @Parameter(description = "debug") @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, - @RequestHeader(HEADER_USER_ID) String userId) throws JsonProcessingException { + @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "debug") @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, + @RequestHeader(HEADER_USER_ID) String userId) throws JsonProcessingException { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(DYNAMIC_MARGIN_CALCULATION, userId); studyService.assertCanRunOnConstructionNode(studyUuid, nodeUuid, List.of(DYNAWO_PROVIDER), studyService::getDynamicMarginCalculationProvider); studyService.runDynamicMarginCalculation(studyUuid, nodeUuid, rootNetworkUuid, userId, debug); return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).build(); @@ -2134,8 +2144,8 @@ public ResponseEntity runDynamicMarginCalculation(@Parameter(description = @ApiResponse(responseCode = "204", description = "No dynamic margin calculation status"), @ApiResponse(responseCode = "404", description = "The dynamic margin calculation has not been found")}) public ResponseEntity getDynamicMarginCalculationStatus(@Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, - @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "root network id") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { String result = rootNetworkNodeInfoService.getDynamicMarginCalculationStatus(nodeUuid, rootNetworkUuid); return result != null ? ResponseEntity.ok().body(result) : ResponseEntity.noContent().build(); } @@ -2212,37 +2222,6 @@ public ResponseEntity> getOptionalServices() { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(remoteServicesInspector.getOptionalServices()); } - static class MyEnumConverter> extends PropertyEditorSupport { - private final Class enumClass; - - MyEnumConverter(Class enumClass) { - this.enumClass = enumClass; - } - - @Override - public void setAsText(final String text) throws IllegalArgumentException { - try { - E value = Enum.valueOf(enumClass, text.toUpperCase()); - setValue(value); - } catch (IllegalArgumentException ex) { - String avail = StringUtils.join(enumClass.getEnumConstants(), ", "); - throw new IllegalArgumentException(String.format("Enum unknown entry '%s' should be among %s", text, avail)); - } - } - } - - static class MyModificationTypeConverter extends PropertyEditorSupport { - - MyModificationTypeConverter() { - super(); - } - - @Override - public void setAsText(final String text) throws IllegalArgumentException { - setValue(ModificationType.getTypeFromUri(text)); - } - } - @GetMapping(value = "/studies/{studyUuid}/sensitivity-analysis/parameters") @Operation(summary = "Get sensitivity analysis parameters on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The sensitivity analysis parameters")}) @@ -2271,7 +2250,8 @@ public ResponseEntity getSensitivityAnalysisFactorCount( @PathVariable("nodeUuid") UUID nodeUuid, @RequestBody String sensitivityAnalysisParameters) { return ResponseEntity.ok().body(sensitivityAnalysisService.getSensitivityAnalysisFactorCount(rootNetworkService.getNetworkUuid(rootNetworkUuid), - networkModificationTreeService.getVariantId(nodeUuid, rootNetworkUuid), sensitivityAnalysisParameters)); + networkModificationTreeService.getVariantId(nodeUuid, rootNetworkUuid), + sensitivityAnalysisParameters)); } @GetMapping(value = "/servers/infos") @@ -2318,7 +2298,7 @@ public ResponseEntity evaluateFilter( } @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/global-filter/evaluate", - produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Evaluate a global filter to get matched elements") @ApiResponse(responseCode = "200", description = "The list of matched elements") public ResponseEntity> evaluateGlobalFilter( @@ -2371,7 +2351,7 @@ public ResponseEntity exportFilters( @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, @Parameter(description = "Filters uuid to be resolved") @RequestParam("filtersUuid") List filtersUuid, @Parameter(description = "Should get in upstream built node ?") @RequestParam(value = "inUpstreamBuiltParentNode", required = false, - defaultValue = "false") boolean inUpstreamBuiltParentNode) { + defaultValue = "false") boolean inUpstreamBuiltParentNode) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.exportFilters(rootNetworkUuid, filtersUuid, nodeUuid, inUpstreamBuiltParentNode)); } @@ -2379,11 +2359,12 @@ public ResponseEntity exportFilters( @Operation(summary = "run state estimation on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The state estimation has started")}) public ResponseEntity runStateEstimation(@Parameter(description = "studyUuid") @PathVariable("studyUuid") UUID studyUuid, - @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, - @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, - @RequestHeader(HEADER_USER_ID) String userId) { + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @RequestParam(name = "debug", required = false, defaultValue = "false") boolean debug, + @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(STATE_ESTIMATION, userId); studyService.runStateEstimation(studyUuid, nodeUuid, rootNetworkUuid, userId, debug); return ResponseEntity.ok().build(); } @@ -2392,11 +2373,12 @@ public ResponseEntity runStateEstimation(@Parameter(description = "studyUu @Operation(summary = "run pcc min on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The pcc min has started")}) public ResponseEntity runPccMin(@Parameter(description = "studyUuid") @PathVariable("studyUuid") UUID studyUuid, - @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, @RequestHeader(HEADER_USER_ID) String userId) { studyService.assertIsNodeNotReadOnly(nodeUuid); + studyService.assertOnQuotasAvailability(PCC_MIN, userId); studyService.runPccMin(studyUuid, nodeUuid, rootNetworkUuid, userId); return ResponseEntity.ok().build(); } @@ -2405,8 +2387,8 @@ public ResponseEntity runPccMin(@Parameter(description = "studyUuid") @Pat @Operation(summary = "stop pcc min on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The pcc min has been stopped")}) public ResponseEntity stopPccMin(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { rootNetworkNodeInfoService.stopPccMin(studyUuid, nodeUuid, rootNetworkUuid); return ResponseEntity.ok().build(); } @@ -2424,7 +2406,7 @@ public ResponseEntity getPccMinResult(@Parameter(description = "study UU Pageable pageable) { String result = rootNetworkNodeInfoService.getPccMinResult(nodeUuid, rootNetworkUuid, filters, globalFilters, pageable); return result != null ? ResponseEntity.ok().body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/state-estimation/result") @@ -2434,10 +2416,10 @@ public ResponseEntity getPccMinResult(@Parameter(description = "study UU @ApiResponse(responseCode = "404", description = "The state estimation has not been found")}) public ResponseEntity getStateEstimationResult(@Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { String result = rootNetworkNodeInfoService.getStateEstimationResult(nodeUuid, rootNetworkUuid); return result != null ? ResponseEntity.ok().body(result) : - ResponseEntity.noContent().build(); + ResponseEntity.noContent().build(); } @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/state-estimation/status") @@ -2446,8 +2428,8 @@ public ResponseEntity getStateEstimationResult(@Parameter(description = @ApiResponse(responseCode = "204", description = "No state estimation has been done yet"), @ApiResponse(responseCode = "404", description = "The state estimation status has not been found")}) public ResponseEntity getStateEstimationStatus(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, - @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { String status = rootNetworkNodeInfoService.getStateEstimationStatus(nodeUuid, rootNetworkUuid); return status != null ? ResponseEntity.ok().body(status) : ResponseEntity.noContent().build(); } @@ -2468,8 +2450,8 @@ public ResponseEntity getPccMinStatus(@Parameter(description = "Study UU @Operation(summary = "stop state estimation on study") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The state estimation has been stopped")}) public ResponseEntity stopStateEstimation(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, - @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { rootNetworkNodeInfoService.stopStateEstimation(studyUuid, nodeUuid, rootNetworkUuid); return ResponseEntity.ok().build(); } @@ -2566,4 +2548,42 @@ public ResponseEntity> getAllComputationsStatus(@Pa @Parameter(description = "Node UUID") @PathVariable("nodeUuid") UUID nodeUuid) { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getAllComputationsStatus(studyUuid, rootNetworkUuid, nodeUuid)); } + + @GetMapping(value = "/studies/quota/status") + @Operation(summary = "Get operation quota status (enabled/disabled)") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "status returnd")}) + public ResponseEntity getOperationQuotaStatus() { + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.getOperationQuotaStatus()); + } + + static class MyEnumConverter> extends PropertyEditorSupport { + private final Class enumClass; + + MyEnumConverter(Class enumClass) { + this.enumClass = enumClass; + } + + @Override + public void setAsText(final String text) throws IllegalArgumentException { + try { + E value = Enum.valueOf(enumClass, text.toUpperCase()); + setValue(value); + } catch (IllegalArgumentException ex) { + String avail = StringUtils.join(enumClass.getEnumConstants(), ", "); + throw new IllegalArgumentException(String.format("Enum unknown entry '%s' should be among %s", text, avail)); + } + } + } + + static class MyModificationTypeConverter extends PropertyEditorSupport { + + MyModificationTypeConverter() { + super(); + } + + @Override + public void setAsText(final String text) throws IllegalArgumentException { + setValue(ModificationType.getTypeFromUri(text)); + } + } } diff --git a/src/main/java/org/gridsuite/study/server/dto/OperationType.java b/src/main/java/org/gridsuite/study/server/dto/OperationType.java new file mode 100644 index 0000000000..c26b907214 --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/dto/OperationType.java @@ -0,0 +1,41 @@ +/** + * 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.study.server.dto; + +/** + * @author Ghiles Abdellah {@literal } + */ +public enum OperationType { + CASES, + BUILD, + LOAD_FLOW, + SECURITY, + SENSITIVITY, + SHORT_CIRCUIT, + VOLTAGE_INIT, + PCC_MIN, + STATE_ESTIMATION, + BALANCE_ADJUSTEMENT, + DYNAMIC_SIMULATION, + DYNAMIC_SECURITY, + DYNAMIC_MARGIN; + + public static OperationType mapFromComputationType(ComputationType computationType) { + return switch (computationType) { + case LOAD_FLOW -> OperationType.LOAD_FLOW; + case SECURITY_ANALYSIS -> OperationType.SECURITY; + case SENSITIVITY_ANALYSIS -> OperationType.SENSITIVITY; + case VOLTAGE_INITIALIZATION -> OperationType.VOLTAGE_INIT; + case DYNAMIC_SIMULATION -> OperationType.DYNAMIC_SIMULATION; + case DYNAMIC_SECURITY_ANALYSIS -> OperationType.DYNAMIC_SECURITY; + case DYNAMIC_MARGIN_CALCULATION -> OperationType.DYNAMIC_MARGIN; + case STATE_ESTIMATION -> OperationType.STATE_ESTIMATION; + case PCC_MIN -> OperationType.PCC_MIN; + case SHORT_CIRCUIT, SHORT_CIRCUIT_ONE_BUS -> OperationType.SHORT_CIRCUIT; + }; + } +} diff --git a/src/main/java/org/gridsuite/study/server/dto/UserProfileInfos.java b/src/main/java/org/gridsuite/study/server/dto/UserProfileInfos.java index d5136b2922..4a8b9155ec 100644 --- a/src/main/java/org/gridsuite/study/server/dto/UserProfileInfos.java +++ b/src/main/java/org/gridsuite/study/server/dto/UserProfileInfos.java @@ -11,6 +11,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.Map; import java.util.UUID; /** @@ -45,11 +46,11 @@ public class UserProfileInfos { private UUID voltageInitParameterId; - Integer maxAllowedBuilds; - private UUID spreadsheetConfigCollectionId; private UUID networkVisualizationParameterId; private UUID workspaceId; + + private Map maxComputationQuotas; } diff --git a/src/main/java/org/gridsuite/study/server/error/StudyBusinessErrorCode.java b/src/main/java/org/gridsuite/study/server/error/StudyBusinessErrorCode.java index 099ed6f237..927ab00f1b 100644 --- a/src/main/java/org/gridsuite/study/server/error/StudyBusinessErrorCode.java +++ b/src/main/java/org/gridsuite/study/server/error/StudyBusinessErrorCode.java @@ -31,7 +31,8 @@ public enum StudyBusinessErrorCode implements BusinessErrorCode { TOO_MANY_NAD_CONFIGS("study.tooManyNadConfigs"), TOO_MANY_MAP_CARDS("study.tooManyMapCards"), ELEMENT_ALREADY_EXISTS("study.elementAlreadyExists"), - UNPROCESSABLE_IMPORT_PARAMETER("study.unprocessableImportParameter"); + UNPROCESSABLE_IMPORT_PARAMETER("study.unprocessableImportParameter"), + MAX_OPERATION_TYPE_EXCEEDED("study.maxOperationTypeExceeded"); private final String value; diff --git a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java index c682995a97..5f6b208765 100644 --- a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java +++ b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java @@ -69,6 +69,7 @@ public class ConsumerService { private final RootNetworkNodeInfoService rootNetworkNodeInfoService; private final DirectoryService directoryService; private final ComputationParametersService computationParametersService; + private final UserAdminService userAdminService; public ConsumerService(ObjectMapper objectMapper, NotificationService notificationService, @@ -79,7 +80,8 @@ public ConsumerService(ObjectMapper objectMapper, StudyConfigService studyConfigService, RootNetworkNodeInfoService rootNetworkNodeInfoService, DirectoryService directoryService, - ComputationParametersService computationParametersService) { + ComputationParametersService computationParametersService, + UserAdminService userAdminService) { this.objectMapper = objectMapper; this.notificationService = notificationService; this.studyService = studyService; @@ -90,6 +92,7 @@ public ConsumerService(ObjectMapper objectMapper, this.rootNetworkNodeInfoService = rootNetworkNodeInfoService; this.directoryService = directoryService; this.computationParametersService = computationParametersService; + this.userAdminService = userAdminService; } @Bean @@ -396,6 +399,11 @@ public void consumeCalculationFailed(Message msg, ComputationType comput if (receiverObj != null) { handleUnblockNode(receiverObj, computationType); + // free quota + if (userId != null) { + userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid); + } + // send notification for failed computation UUID studyUuid = networkModificationTreeService.getStudyUuidForNodeId(receiverObj.getNodeUuid()); notificationService.emitStudyError(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateFailedType(), errorMessage, userId); @@ -417,6 +425,14 @@ public void consumeCalculationStopped(Message msg, ComputationType compu // send notification for stopped computation notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateStatusType()); + // free quota + String resultId = msg.getHeaders().get(RESULT_UUID, String.class); + String userId = msg.getHeaders().get(HEADER_USER_ID, String.class); + if (resultId != null && userId != null) { + UUID resultUuid = UUID.fromString(resultId); + userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid); + } + LOGGER.info("{} stopped for node '{}'", computationType.getLabel(), receiverObj.getNodeUuid()); } catch (JsonProcessingException e) { LOGGER.error(e.toString()); @@ -469,6 +485,12 @@ public void consumeCalculationDebug(Message msg, ComputationType computa String errorMessage = (String) msg.getHeaders().get(HEADER_ERROR_MESSAGE); String userId = (String) msg.getHeaders().get(HEADER_USER_ID); + + // free quota + if (userId != null) { + userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid); + } + notificationService.emitComputationDebugFileStatus(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType, userId, resultUuid, errorMessage); })); } @@ -500,6 +522,12 @@ public void consumeCalculationResult(Message msg, ComputationType comput handleLoadFlowSuccess(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), resultUuid, userId); } + // free quota + String userId = msg.getHeaders().get(HEADER_USER_ID, String.class); + if (userId != null) { + userAdminService.endOperationWithQuota(userId, OperationType.mapFromComputationType(computationType), resultUuid); + } + // send notifications notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateStatusType()); notificationService.emitStudyChanged(studyUuid, receiverObj.getNodeUuid(), receiverObj.getRootNetworkUuid(), computationType.getUpdateResultType()); diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index d838d07c6f..d7d7dc1d05 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -61,6 +61,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -165,6 +166,9 @@ public enum ReportType { private final StudyService self; + @Value("${study.enable-operation-quotas}") + private Boolean shouldCheckOperationQuotas = true; + @Autowired public StudyService( StudyRepository studyRepository, @@ -1009,6 +1013,7 @@ private void handleLoadflowRequest(StudyEntity studyEntity, UUID nodeUuid, UUID new LoadFlowService.ParametersInfos(lfParametersUuid, withRatioTapChangers, isSecurityNode), lfReportUuid, userId); rootNetworkNodeInfoService.updateLoadflowResultUuid(nodeUuid, rootNetworkUuid, result, withRatioTapChangers); + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(LOAD_FLOW), result); notificationService.emitStudyChanged(studyEntity.getId(), nodeUuid, rootNetworkUuid, LOAD_FLOW.getUpdateStatusType()); notificationService.emitElementUpdated(studyEntity.getId(), userId); } @@ -1324,7 +1329,10 @@ public UUID runSecurityAnalysis(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, StudyEntity study = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleSecurityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId); + UUID result = handleSecurityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SECURITY_ANALYSIS), result); + return result; } private UUID handleSecurityAnalysisRequest(StudyEntity study, UUID nodeUuid, UUID rootNetworkUuid, String userId) { @@ -1737,20 +1745,25 @@ public void handleBuildSuccess(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUu } private long getAllowedBuildNodesUpToQuota(@NonNull UUID studyUuid, @NonNull UUID rootNetworkUuid, @NonNull String userId) { - return userAdminService.getUserMaxAllowedBuilds(userId).map(maxBuilds -> { + Map userMaxQuotas = userAdminService.getUserMaxQuota(userId); + + return Optional.ofNullable(userMaxQuotas.get(OperationType.BUILD)).map(maxBuilds -> { long nbBuiltNodes = networkModificationTreeService.countBuiltNodes(studyUuid, rootNetworkUuid); return maxBuilds - nbBuiltNodes; }).orElse(Long.MAX_VALUE); } public void assertNoMaxBuilds(@NonNull UUID studyUuid, @NonNull UUID rootNetworkUuid, @NonNull String userId) { + Map userMaxQuotas = userAdminService.getUserMaxQuota(userId); + // check restrictions on node builds number - userAdminService.getUserMaxAllowedBuilds(userId).ifPresent(maxBuilds -> { + Integer maxBuilds = userMaxQuotas.get(OperationType.BUILD); + if (maxBuilds != null) { long nbBuiltNodes = networkModificationTreeService.countBuiltNodes(studyUuid, rootNetworkUuid); if (nbBuiltNodes >= maxBuilds) { throw new StudyException(MAX_NODE_BUILDS_EXCEEDED, "max allowed built nodes reached", Map.of("limit", maxBuilds)); } - }); + } } @Transactional @@ -2601,7 +2614,10 @@ public UUID runSensitivityAnalysis(@NonNull UUID studyUuid, @NonNull UUID nodeUu StudyEntity study = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleSensitivityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId); + UUID result = handleSensitivityAnalysisRequest(study, nodeUuid, rootNetworkUuid, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SENSITIVITY_ANALYSIS), result); + return result; } private UUID handleSensitivityAnalysisRequest(StudyEntity study, UUID nodeUuid, UUID rootNetworkUuid, String userId) { @@ -2634,7 +2650,10 @@ public UUID runShortCircuit(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid, StudyEntity studyEntity = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleShortCircuitRequest(studyEntity, nodeUuid, rootNetworkUuid, busId, debug, userId); + UUID result = handleShortCircuitRequest(studyEntity, nodeUuid, rootNetworkUuid, busId, debug, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(SHORT_CIRCUIT), result); + return result; } private UUID handleShortCircuitRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, Optional busId, boolean debug, String userId) { @@ -2661,7 +2680,10 @@ public UUID runVoltageInit(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid, StudyEntity studyEntity = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleVoltageInitRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + UUID result = handleVoltageInitRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(VOLTAGE_INITIALIZATION), result); + return result; } private UUID handleVoltageInitRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) { @@ -2949,7 +2971,10 @@ public UUID runDynamicSimulation(@NonNull UUID studyUuid, @NonNull UUID nodeUuid StudyEntity studyEntity = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleDynamicSimulationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + UUID result = handleDynamicSimulationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_SIMULATION), result); + return result; } private UUID handleDynamicSimulationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) { @@ -3021,7 +3046,10 @@ public UUID runDynamicSecurityAnalysis(@NonNull UUID studyUuid, @NonNull UUID no StudyEntity studyEntity = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleDynamicSecurityAnalysisRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + UUID result = handleDynamicSecurityAnalysisRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_SECURITY_ANALYSIS), result); + return result; } private UUID handleDynamicSecurityAnalysisRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) { @@ -3100,7 +3128,10 @@ public UUID runDynamicMarginCalculation(@NonNull UUID studyUuid, @NonNull UUID n StudyEntity studyEntity = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleDynamicMarginCalculationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + UUID result = handleDynamicMarginCalculationRequest(studyEntity, nodeUuid, rootNetworkUuid, debug, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(DYNAMIC_MARGIN_CALCULATION), result); + return result; } private UUID handleDynamicMarginCalculationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, boolean debug, String userId) { @@ -3322,7 +3353,10 @@ public UUID runStateEstimation(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, StudyEntity studyEntity = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handleStateEstimationRequest(studyEntity, nodeUuid, rootNetworkUuid, userId, debug); + UUID result = handleStateEstimationRequest(studyEntity, nodeUuid, rootNetworkUuid, userId, debug); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(STATE_ESTIMATION), result); + return result; } @Transactional @@ -3330,7 +3364,10 @@ public UUID runPccMin(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, @NonNull StudyEntity studyEntity = getStudy(studyUuid); networkModificationTreeService.blockNode(rootNetworkUuid, nodeUuid); - return handlePccMinRequest(studyEntity, nodeUuid, rootNetworkUuid, userId); + UUID result = handlePccMinRequest(studyEntity, nodeUuid, rootNetworkUuid, userId); + + userAdminService.startOperationWithQuota(userId, OperationType.mapFromComputationType(PCC_MIN), result); + return result; } private UUID handleStateEstimationRequest(StudyEntity studyEntity, UUID nodeUuid, UUID rootNetworkUuid, String userId, boolean debug) { @@ -3734,4 +3771,26 @@ public void invalidateStudyRootNetwork(UUID studyUuid, UUID rootNetworkUuid, Str rootNetworkService.updateRootNetworkIndexationStatus(studyUuid, rootNetworkUuid, RootNetworkIndexationStatus.NOT_INDEXED); notificationService.emitRootNetworksUpdated(studyUuid); } + + public void assertOnQuotasAvailability(ComputationType computationType, String userId) { + if (!shouldCheckOperationQuotas) { + return; + } + + Map userMaxQuotas = userAdminService.getUserMaxQuota(userId); + Map userCurrentQuotas = userAdminService.getUserCurrentQuota(userId); + OperationType quotaType = OperationType.mapFromComputationType(computationType); + + Integer maxComputation = userMaxQuotas.get(quotaType); + Integer currentComputation = userCurrentQuotas.get(quotaType); + + if (maxComputation != null && currentComputation != null && currentComputation >= maxComputation) { + throw new StudyException(MAX_OPERATION_TYPE_EXCEEDED, "Max number of " + computationType.name() + " already reached", + Map.of("maxComputation", maxComputation, "currentComputation", currentComputation)); + } + } + + public Boolean getOperationQuotaStatus() { + return shouldCheckOperationQuotas; + } } diff --git a/src/main/java/org/gridsuite/study/server/service/UserAdminService.java b/src/main/java/org/gridsuite/study/server/service/UserAdminService.java index 0f6cdac357..f957df8af6 100644 --- a/src/main/java/org/gridsuite/study/server/service/UserAdminService.java +++ b/src/main/java/org/gridsuite/study/server/service/UserAdminService.java @@ -8,14 +8,19 @@ package org.gridsuite.study.server.service; import org.gridsuite.study.server.RemoteServicesProperties; +import org.gridsuite.study.server.dto.OperationType; import org.gridsuite.study.server.dto.UserProfileInfos; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -import java.util.Optional; +import java.util.Map; +import java.util.UUID; -import static org.gridsuite.study.server.StudyConstants.*; +import static org.gridsuite.study.server.StudyConstants.DELIMITER; +import static org.gridsuite.study.server.StudyConstants.USER_ADMIN_API_VERSION; /** * @author David Braquart @@ -23,8 +28,12 @@ @Service public class UserAdminService { private static final String USERS_PROFILE_URI = "/users/{sub}/profile"; + private static final String USERS_QUOTA_URI = "/users/{sub}/quota"; + private static final String USERS_MAX_QUOTA_URI = USERS_QUOTA_URI + "/max"; + private static final String USERS_CURRENT_QUOTA_URI = USERS_QUOTA_URI + "/current"; + private static final String USERS_START_QUOTA_URI = USERS_QUOTA_URI + "/{operation}/{operation_id}/start"; + private static final String USERS_END_QUOTA_URI = USERS_QUOTA_URI + "/{operation}/{operation_id}/end"; - private static final String USERS_MAX_ALLOWED_BUILDS_URI = USERS_PROFILE_URI + "/max-builds"; private final RestTemplate restTemplate; private String userAdminServerBaseUri; @@ -39,13 +48,43 @@ public void setUserAdminServerBaseUri(String serverBaseUri) { public UserProfileInfos getUserProfile(String sub) { String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + USERS_PROFILE_URI) - .buildAndExpand(sub).toUriString(); + .buildAndExpand(sub).toUriString(); return restTemplate.getForObject(userAdminServerBaseUri + path, UserProfileInfos.class); } - public Optional getUserMaxAllowedBuilds(String sub) { - String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + USERS_MAX_ALLOWED_BUILDS_URI) - .buildAndExpand(sub).toUriString(); - return Optional.ofNullable(restTemplate.getForObject(userAdminServerBaseUri + path, Integer.class)); + public Map getUserMaxQuota(String sub) { + String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + USERS_MAX_QUOTA_URI) + .buildAndExpand(sub).toUriString(); + return restTemplate.exchange( + userAdminServerBaseUri + path, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Map getUserCurrentQuota(String sub) { + String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + USERS_CURRENT_QUOTA_URI) + .buildAndExpand(sub).toUriString(); + return restTemplate.exchange( + userAdminServerBaseUri + path, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public void startOperationWithQuota(String sub, OperationType quotaType, UUID operationId) { + String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + USERS_START_QUOTA_URI) + .buildAndExpand(sub, quotaType, operationId) + .toUriString(); + restTemplate.postForEntity(userAdminServerBaseUri + path, null, Void.class); + } + + public void endOperationWithQuota(String sub, OperationType quotaType, UUID operationId) { + String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + USERS_END_QUOTA_URI) + .buildAndExpand(sub, quotaType, operationId) + .toUriString(); + restTemplate.postForEntity(userAdminServerBaseUri + path, null, Void.class); } } diff --git a/src/main/resources/config/application.yaml b/src/main/resources/config/application.yaml index c1cd9ad0ac..e528bd0e6e 100644 --- a/src/main/resources/config/application.yaml +++ b/src/main/resources/config/application.yaml @@ -253,3 +253,4 @@ gridsuite: study: max-root-network-by-study: 4 + enable-operation-quotas: ${ENABLE_OPERATION_QUOTAS:true} diff --git a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java index 823d25a96d..80d36d206e 100644 --- a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java +++ b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java @@ -316,19 +316,19 @@ void setup() { buildStopStubId = wireMockServer.stubFor(WireMock.put(WireMock.urlPathEqualTo("/v1/build/stop")) .withPostServeAction(POST_ACTION_SEND_INPUT, Map.of("payload", "", "destination", "build.stopped")) .willReturn(WireMock.ok())).getId(); - userProfileQuotaStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID + "/profile/max-builds")) + userProfileQuotaStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID + "/quota/max")) .willReturn(WireMock.ok() - .withBody("10") + .withBody("{\"BUILD\": 10}") .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))).getId(); - userProfileQuotaExceededStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID_QUOTA_EXCEEDED + "/profile/max-builds")) + userProfileQuotaExceededStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID_QUOTA_EXCEEDED + "/quota/max")) .willReturn(WireMock.ok() - .withBody("1") + .withBody("{\"BUILD\": 1}") .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))).getId(); - userProfileNoQuotaStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID_NO_QUOTA + "/profile/max-builds")) + userProfileNoQuotaStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID_NO_QUOTA + "/quota/max")) .willReturn(WireMock.ok() - .withBody((String) null) + .withBody("{}") .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))).getId(); - wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID_NO_PROFILE + "/profile/max-builds")) + wireMockServer.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/users/" + USER_ID_NO_PROFILE + "/quota/max")) .willReturn(WireMock.notFound())).getId(); } @@ -386,7 +386,7 @@ void testBuildQuotaExceeded() throws Exception { assertEquals(MAX_NODE_BUILDS_EXCEEDED.value(), problemDetail.getBusinessErrorCode()); assertEquals(1, problemDetail.getBusinessErrorValues().get("limit")); assertEquals("max allowed built nodes reached", problemDetail.getDetail()); - WireMockUtils.verifyGetRequest(wireMockServer, userProfileQuotaExceededStubId, "/v1/users/" + userId + "/profile/max-builds", Map.of()); + WireMockUtils.verifyGetRequest(wireMockServer, userProfileQuotaExceededStubId, "/v1/users/" + userId + "/quota/max", Map.of()); } @Test @@ -2968,7 +2968,7 @@ private void testBuildWithNodeUuid(UUID studyUuid, UUID nodeUuid, UUID rootNetwo assertEquals(NotificationService.UPDATE_TYPE_BUILD_COMPLETED, buildStatusMessage.getHeaders().get(NotificationService.HEADER_UPDATE_TYPE)); assertEquals(Set.of("s1", "s2"), buildStatusMessage.getHeaders().get(NotificationService.HEADER_UPDATE_TYPE_SUBSTATIONS_IDS)); - WireMockUtils.verifyGetRequest(wireMockServer, profileStubId, "/v1/users/" + userId + "/profile/max-builds", Map.of()); + WireMockUtils.verifyGetRequest(wireMockServer, profileStubId, "/v1/users/" + userId + "/quota/max", Map.of()); WireMockUtils.verifyPostRequest(wireMockServer, buildOkStubId, "/v1/networks/" + NETWORK_UUID_STRING + "/build", Map.of(QUERY_PARAM_RECEIVER, WireMock.matching(".*"))); assertEquals(BuildStatus.BUILT, networkModificationTreeService.getNodeBuildStatus(nodeUuid, rootNetworkUuid).getGlobalBuildStatus()); @@ -2997,7 +2997,7 @@ private void testBuildAndStopWithNodeUuid(UUID studyUuid, UUID nodeUuid, UUID ro assertEquals(Set.of("s1", "s2"), buildStatusMessage.getHeaders().get(NotificationService.HEADER_UPDATE_TYPE_SUBSTATIONS_IDS)); WireMockUtils.verifyPostRequest(wireMockServer, buildOkStubId, "/v1/networks/" + NETWORK_UUID_STRING + "/build", Map.of(QUERY_PARAM_RECEIVER, WireMock.matching(".*"))); - WireMockUtils.verifyGetRequest(wireMockServer, profileStubId, "/v1/users/" + userId + "/profile/max-builds", Map.of()); + WireMockUtils.verifyGetRequest(wireMockServer, profileStubId, "/v1/users/" + userId + "/quota/max", Map.of()); assertEquals(BuildStatus.BUILT, networkModificationTreeService.getNodeBuildStatus(nodeUuid, rootNetworkUuid).getGlobalBuildStatus()); // node is built networkModificationTreeService.updateNodeBuildStatus(nodeUuid, rootNetworkUuid, NodeBuildStatus.from(BuildStatus.BUILDING)); @@ -3045,7 +3045,7 @@ private void testBuildFailedWithNodeUuid(UUID studyUuid, UUID nodeUuid, UUID roo assertEquals(ERROR_MESSAGE, buildStatusMessage.getHeaders().get(NotificationService.HEADER_ERROR)); WireMockUtils.verifyPostRequest(wireMockServer, buildFailedStubId, "/v1/networks/" + NETWORK_UUID_2_STRING + "/build", Map.of(QUERY_PARAM_RECEIVER, WireMock.matching(".*"))); - WireMockUtils.verifyGetRequest(wireMockServer, userProfileQuotaStubId, "/v1/users/" + USER_ID + "/profile/max-builds", Map.of()); + WireMockUtils.verifyGetRequest(wireMockServer, userProfileQuotaStubId, "/v1/users/" + USER_ID + "/quota/max", Map.of()); assertEquals(BuildStatus.NOT_BUILT, networkModificationTreeService.getNodeBuildStatus(nodeUuid, rootNetworkUuid).getGlobalBuildStatus()); // node is not built } @@ -3068,7 +3068,7 @@ private void testBuildErrorWithNodeUuid(UUID studyUuid, UUID nodeUuid, UUID root assertEquals(NotificationService.NODE_BUILD_STATUS_UPDATED, buildStatusMessage.getHeaders().get(NotificationService.HEADER_UPDATE_TYPE)); WireMockUtils.verifyPostRequest(wireMockServer, buildErrorStubId, "/v1/networks/" + NETWORK_UUID_3_STRING + "/build", Map.of(QUERY_PARAM_RECEIVER, WireMock.matching(".*"))); - WireMockUtils.verifyGetRequest(wireMockServer, userProfileQuotaStubId, "/v1/users/" + USER_ID + "/profile/max-builds", Map.of()); + WireMockUtils.verifyGetRequest(wireMockServer, userProfileQuotaStubId, "/v1/users/" + USER_ID + "/quota/max", Map.of()); assertEquals(BuildStatus.NOT_BUILT, networkModificationTreeService.getNodeBuildStatus(nodeUuid, rootNetworkUuid).getGlobalBuildStatus()); // node is not built } diff --git a/src/test/java/org/gridsuite/study/server/NodeSequenceTest.java b/src/test/java/org/gridsuite/study/server/NodeSequenceTest.java index 3749372f99..d84562a47b 100644 --- a/src/test/java/org/gridsuite/study/server/NodeSequenceTest.java +++ b/src/test/java/org/gridsuite/study/server/NodeSequenceTest.java @@ -6,6 +6,7 @@ */ package org.gridsuite.study.server; +import org.gridsuite.study.server.dto.OperationType; import org.gridsuite.study.server.dto.sequence.NodeSequenceType; import org.gridsuite.study.server.dto.sequence.SecuritySequence; import org.gridsuite.study.server.error.StudyException; @@ -34,7 +35,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import java.util.Optional; +import java.util.Map; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -83,7 +84,7 @@ void setUp() { studyEntity = studyRepository.save(study); studyUuid = studyEntity.getId(); - doReturn(Optional.of(10)).when(userAdminService).getUserMaxAllowedBuilds(userId); + doReturn(Map.of(OperationType.BUILD, 10)).when(userAdminService).getUserMaxQuota(userId); } @Test diff --git a/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java b/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java index 20ed2a593a..c6a6e34388 100644 --- a/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java +++ b/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java @@ -48,6 +48,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.web.servlet.MockMvc; import java.util.*; @@ -63,6 +64,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -145,7 +147,7 @@ class ShortCircuitTest implements WithAssertions { @Autowired private ShortCircuitService shortCircuitService; - @Autowired + @MockitoSpyBean private UserAdminService userAdminService; @Autowired @@ -195,6 +197,10 @@ void setup() { shortCircuitService.setShortCircuitServerBaseUri(wireMockServer.baseUrl()); reportService.setReportServerBaseUri(wireMockServer.baseUrl()); userAdminService.setUserAdminServerBaseUri(wireMockServer.baseUrl()); + + // Stub quota operations to avoid HTTP latency inside @Transactional causing race conditions with @PostCompletion notifications + doNothing().when(userAdminService).startOperationWithQuota(any(), any(), any()); + doNothing().when(userAdminService).endOperationWithQuota(any(), any(), any()); } @Test diff --git a/src/test/java/org/gridsuite/study/server/StateEstimationTest.java b/src/test/java/org/gridsuite/study/server/StateEstimationTest.java index ae6f78298f..a0589f45b6 100644 --- a/src/test/java/org/gridsuite/study/server/StateEstimationTest.java +++ b/src/test/java/org/gridsuite/study/server/StateEstimationTest.java @@ -225,6 +225,8 @@ public MockResponse dispatch(RecordedRequest request) { return new MockResponse(200, Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), objectMapper.writeValueAsString(STATE_ESTIMATION_PARAMETERS_UUID)); } else if (path.matches("/v1/parameters/default") && "POST".equals(method)) { return new MockResponse(200, Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), objectMapper.writeValueAsString(STATE_ESTIMATION_PARAMETERS_UUID)); + } else if (path.matches("/v1/users/.*/quota/.*") && "POST".equals(method)) { + return new MockResponse(200); } else { return new MockResponse.Builder().code(418).body("Unhandled method+path: " + request.getMethod() + " " + request.getPath()).build(); } diff --git a/src/test/java/org/gridsuite/study/server/StudyControllerDynamicMarginCalculationTest.java b/src/test/java/org/gridsuite/study/server/StudyControllerDynamicMarginCalculationTest.java index cae955e58d..10df59637b 100644 --- a/src/test/java/org/gridsuite/study/server/StudyControllerDynamicMarginCalculationTest.java +++ b/src/test/java/org/gridsuite/study/server/StudyControllerDynamicMarginCalculationTest.java @@ -24,6 +24,7 @@ import org.gridsuite.study.server.service.NetworkModificationTreeService; import org.gridsuite.study.server.service.RootNetworkNodeInfoService; import org.gridsuite.study.server.service.StudyService; +import org.gridsuite.study.server.service.UserAdminService; import org.gridsuite.study.server.service.client.util.UrlUtil; import org.gridsuite.study.server.service.dynamicmargincalculation.DynamicMarginCalculationService; import org.gridsuite.study.server.utils.TestUtils; @@ -114,6 +115,9 @@ class StudyControllerDynamicMarginCalculationTest { @MockitoBean private LoadFlowService mockLoadFlowService; + @MockitoBean + private UserAdminService userAdminService; + @MockitoSpyBean private DynamicMarginCalculationService spyDynamicMarginCalculationService; diff --git a/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSecurityAnalysisTest.java b/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSecurityAnalysisTest.java index 3383cd4e04..8bf33b251c 100644 --- a/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSecurityAnalysisTest.java +++ b/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSecurityAnalysisTest.java @@ -25,6 +25,7 @@ import org.gridsuite.study.server.service.NetworkModificationTreeService; import org.gridsuite.study.server.service.RootNetworkNodeInfoService; import org.gridsuite.study.server.service.StudyService; +import org.gridsuite.study.server.service.UserAdminService; import org.gridsuite.study.server.service.client.util.UrlUtil; import org.gridsuite.study.server.service.dynamicsecurityanalysis.DynamicSecurityAnalysisService; import org.gridsuite.study.server.service.dynamicsimulation.DynamicSimulationService; @@ -116,6 +117,9 @@ class StudyControllerDynamicSecurityAnalysisTest { @MockitoBean private LoadFlowService mockLoadFlowService; + @MockitoBean + private UserAdminService userAdminService; + @MockitoBean private DynamicSimulationService mockDynamicSimulationService; diff --git a/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSimulationTest.java b/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSimulationTest.java index a6d121cf0f..cdc527a129 100644 --- a/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSimulationTest.java +++ b/src/test/java/org/gridsuite/study/server/StudyControllerDynamicSimulationTest.java @@ -32,6 +32,7 @@ import org.gridsuite.study.server.service.NetworkModificationTreeService; import org.gridsuite.study.server.service.RootNetworkNodeInfoService; import org.gridsuite.study.server.service.StudyService; +import org.gridsuite.study.server.service.UserAdminService; import org.gridsuite.study.server.service.client.util.UrlUtil; import org.gridsuite.study.server.service.dynamicsimulation.DynamicSimulationService; import org.gridsuite.study.server.utils.PropertyType; @@ -145,6 +146,9 @@ class StudyControllerDynamicSimulationTest { @MockitoBean private LoadFlowService loadFlowService; + @MockitoBean + private UserAdminService userAdminService; + @MockitoSpyBean private DynamicSimulationService spyDynamicSimulationService; diff --git a/src/test/java/org/gridsuite/study/server/StudyServiceTest.java b/src/test/java/org/gridsuite/study/server/StudyServiceTest.java index f3ede67d3d..36b1f67d41 100644 --- a/src/test/java/org/gridsuite/study/server/StudyServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/StudyServiceTest.java @@ -7,6 +7,9 @@ package org.gridsuite.study.server; import org.gridsuite.study.server.dto.BuildInfos; +import org.gridsuite.study.server.dto.ComputationType; +import org.gridsuite.study.server.dto.OperationType; +import org.gridsuite.study.server.error.StudyException; import org.gridsuite.study.server.networkmodificationtree.dto.BuildStatus; import org.gridsuite.study.server.networkmodificationtree.dto.NodeBuildStatus; import org.gridsuite.study.server.networkmodificationtree.entities.NodeEntity; @@ -17,16 +20,24 @@ import org.gridsuite.study.server.service.StudyService; import org.gridsuite.study.server.service.UserAdminService; import org.gridsuite.study.server.utils.elasticsearch.DisableElasticsearch; +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 org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.util.ReflectionTestUtils; import java.util.List; -import java.util.Optional; +import java.util.Map; import java.util.UUID; +import static org.gridsuite.study.server.error.StudyBusinessErrorCode.MAX_OPERATION_TYPE_EXCEEDED; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; @SpringBootTest @@ -44,6 +55,12 @@ class StudyServiceTest { @MockitoBean private NetworkModificationService networkModificationService; + @AfterEach + void resetOperationQuotasFlag() { + // restore the test-profile default (study.enable-operation-quotas=false) so no state leaks between tests + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", false); + } + @Test void testBuildFirstLevelChildren() { UUID studyUuid = UUID.randomUUID(); @@ -67,7 +84,7 @@ void testBuildFirstLevelChildren() { */ // quota not reached, all first level children of N1 will be built - doReturn(Optional.of(10)).when(userAdminService).getUserMaxAllowedBuilds(userId); + doReturn(Map.of(OperationType.BUILD, 10)).when(userAdminService).getUserMaxQuota(userId); doReturn(0L).when(networkModificationTreeService).countBuiltNodes(studyUuid, rootNetworkUuid); mockNodeBuild(node2.getIdNode(), rootNetworkUuid); @@ -81,7 +98,7 @@ void testBuildFirstLevelChildren() { verify(networkModificationService, times(0)).buildNode(eq(node4.getIdNode()), eq(rootNetworkUuid), any(), eq(null)); // 1 to check how many children will be built, then 1 for each built children - verify(userAdminService, times(3)).getUserMaxAllowedBuilds(userId); + verify(userAdminService, times(3)).getUserMaxQuota(userId); verify(networkModificationTreeService, times(3)).countBuiltNodes(studyUuid, rootNetworkUuid); } @@ -108,7 +125,7 @@ void testBuildFirstLevelChildrenWithQuotaAlreadyReached() { */ // quota already reached, nothing will be built - doReturn(Optional.of(10)).when(userAdminService).getUserMaxAllowedBuilds(userId); + doReturn(Map.of(OperationType.BUILD, 10)).when(userAdminService).getUserMaxQuota(userId); doReturn(10L).when(networkModificationTreeService).countBuiltNodes(studyUuid, rootNetworkUuid); studyService.buildFirstLevelChildren(studyUuid, node1.getIdNode(), rootNetworkUuid, userId); @@ -117,7 +134,7 @@ void testBuildFirstLevelChildrenWithQuotaAlreadyReached() { verify(networkModificationService, times(0)).buildNode(eq(node3.getIdNode()), eq(rootNetworkUuid), any(), eq(null)); verify(networkModificationService, times(0)).buildNode(eq(node4.getIdNode()), eq(rootNetworkUuid), any(), eq(null)); - verify(userAdminService, times(1)).getUserMaxAllowedBuilds(userId); + verify(userAdminService, times(1)).getUserMaxQuota(userId); verify(networkModificationTreeService, times(1)).countBuiltNodes(studyUuid, rootNetworkUuid); } @@ -144,7 +161,7 @@ void testBuildFirstLevelChildrenWithQuotaReached() { */ // quota will be reached, only one child will be built - doReturn(Optional.of(10)).when(userAdminService).getUserMaxAllowedBuilds(userId); + doReturn(Map.of(OperationType.BUILD, 10)).when(userAdminService).getUserMaxQuota(userId); doReturn(9L).when(networkModificationTreeService).countBuiltNodes(studyUuid, rootNetworkUuid); mockNodeBuild(node2.getIdNode(), rootNetworkUuid); @@ -156,10 +173,92 @@ void testBuildFirstLevelChildrenWithQuotaReached() { verify(networkModificationService, times(0)).buildNode(eq(node4.getIdNode()), eq(rootNetworkUuid), any(), eq(null)); // 1 to check how many children will be built, then 1 for each built children - verify(userAdminService, times(2)).getUserMaxAllowedBuilds(userId); + verify(userAdminService, times(2)).getUserMaxQuota(userId); verify(networkModificationTreeService, times(2)).countBuiltNodes(studyUuid, rootNetworkUuid); } + @Test + void testAssertOnQuotasAvailabilityDoesNothingWhenQuotasCheckDisabled() { + String userId = "userId"; + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", false); + + // quotas check is disabled: even a saturated quota must not throw, and must not even be looked up + assertDoesNotThrow(() -> studyService.assertOnQuotasAvailability(ComputationType.SHORT_CIRCUIT, userId)); + + verify(userAdminService, never()).getUserMaxQuota(userId); + verify(userAdminService, never()).getUserCurrentQuota(userId); + } + + @Test + void testAssertOnQuotasAvailabilityDoesNotThrowWhenUnderQuota() { + String userId = "userId"; + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", true); + + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 5)).when(userAdminService).getUserMaxQuota(userId); + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 2)).when(userAdminService).getUserCurrentQuota(userId); + + assertDoesNotThrow(() -> studyService.assertOnQuotasAvailability(ComputationType.SHORT_CIRCUIT, userId)); + } + + @Test + void testAssertOnQuotasAvailabilityThrowsWhenQuotaReached() { + String userId = "userId"; + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", true); + + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 5)).when(userAdminService).getUserMaxQuota(userId); + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 5)).when(userAdminService).getUserCurrentQuota(userId); + + StudyException exception = assertThrows(StudyException.class, + () -> studyService.assertOnQuotasAvailability(ComputationType.SHORT_CIRCUIT, userId)); + + assertEquals(MAX_OPERATION_TYPE_EXCEEDED, exception.getBusinessErrorCode()); + assertEquals(Map.of("maxComputation", 5, "currentComputation", 5), exception.getBusinessErrorValues()); + } + + @Test + void testAssertOnQuotasAvailabilityThrowsWhenQuotaExceeded() { + String userId = "userId"; + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", true); + + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 5)).when(userAdminService).getUserMaxQuota(userId); + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 6)).when(userAdminService).getUserCurrentQuota(userId); + + assertThrows(StudyException.class, () -> studyService.assertOnQuotasAvailability(ComputationType.SHORT_CIRCUIT, userId)); + } + + @Test + void testAssertOnQuotasAvailabilityDoesNotThrowWhenNoMaxQuotaConfiguredForOperation() { + String userId = "userId"; + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", true); + + // max quotas map has no entry at all for SHORT_CIRCUIT (only for another operation) + doReturn(Map.of(OperationType.LOAD_FLOW, 5)).when(userAdminService).getUserMaxQuota(userId); + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 100)).when(userAdminService).getUserCurrentQuota(userId); + + assertDoesNotThrow(() -> studyService.assertOnQuotasAvailability(ComputationType.SHORT_CIRCUIT, userId)); + } + + @Test + void testAssertOnQuotasAvailabilityDoesNotThrowWhenNoCurrentQuotaReported() { + String userId = "userId"; + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", true); + + doReturn(Map.of(OperationType.SHORT_CIRCUIT, 5)).when(userAdminService).getUserMaxQuota(userId); + // current quotas map has no entry at all for SHORT_CIRCUIT (nothing running yet) + doReturn(Map.of()).when(userAdminService).getUserCurrentQuota(userId); + + assertDoesNotThrow(() -> studyService.assertOnQuotasAvailability(ComputationType.SHORT_CIRCUIT, userId)); + } + + @Test + void testGetOperationQuotaStatusReflectsConfiguredFlag() { + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", true); + assertTrue(studyService.getOperationQuotaStatus()); + + ReflectionTestUtils.setField(studyService, "shouldCheckOperationQuotas", false); + assertFalse(studyService.getOperationQuotaStatus()); + } + private void mockNodeBuild(UUID nodeUuid, UUID rootNetworkUuid) { doReturn(new BuildInfos()).when(networkModificationTreeService).getBuildInfos(nodeUuid, rootNetworkUuid); doNothing().when(networkModificationTreeService).setModificationReports(eq(nodeUuid), eq(rootNetworkUuid), any()); diff --git a/src/test/java/org/gridsuite/study/server/VoltageInitTest.java b/src/test/java/org/gridsuite/study/server/VoltageInitTest.java index 3b20f43163..191b15907b 100644 --- a/src/test/java/org/gridsuite/study/server/VoltageInitTest.java +++ b/src/test/java/org/gridsuite/study/server/VoltageInitTest.java @@ -410,6 +410,8 @@ public MockResponse dispatch(RecordedRequest request) { return new MockResponse(200, Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), objectMapper.writeValueAsString(VOLTAGE_INIT_PARAMETERS_UUID)); } else if (path.matches("/v1/network-modifications/index\\?networkUuid=.*") && "DELETE".equals(method)) { return new MockResponse(200); + } else if (path.matches("/v1/users/.*/quota/.*") && "POST".equals(method)) { + return new MockResponse(200); } else { LOGGER.error("Unhandled method+path: {} {}", request.getMethod(), request.getPath()); return new MockResponse.Builder().code(418).body("Unhandled method+path: " + request.getMethod() + " " + request.getPath()).build(); diff --git a/src/test/java/org/gridsuite/study/server/dto/OperationTypeTest.java b/src/test/java/org/gridsuite/study/server/dto/OperationTypeTest.java new file mode 100644 index 0000000000..08a4842aac --- /dev/null +++ b/src/test/java/org/gridsuite/study/server/dto/OperationTypeTest.java @@ -0,0 +1,29 @@ +/** + * 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.study.server.dto; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class OperationTypeTest { + + @Test + void shouldMapFromComputationTypeCorrectly() { + assertEquals(OperationType.LOAD_FLOW, OperationType.mapFromComputationType(ComputationType.LOAD_FLOW)); + assertEquals(OperationType.SECURITY, OperationType.mapFromComputationType(ComputationType.SECURITY_ANALYSIS)); + assertEquals(OperationType.SENSITIVITY, OperationType.mapFromComputationType(ComputationType.SENSITIVITY_ANALYSIS)); + assertEquals(OperationType.VOLTAGE_INIT, OperationType.mapFromComputationType(ComputationType.VOLTAGE_INITIALIZATION)); + assertEquals(OperationType.DYNAMIC_SIMULATION, OperationType.mapFromComputationType(ComputationType.DYNAMIC_SIMULATION)); + assertEquals(OperationType.DYNAMIC_SECURITY, OperationType.mapFromComputationType(ComputationType.DYNAMIC_SECURITY_ANALYSIS)); + assertEquals(OperationType.DYNAMIC_MARGIN, OperationType.mapFromComputationType(ComputationType.DYNAMIC_MARGIN_CALCULATION)); + assertEquals(OperationType.STATE_ESTIMATION, OperationType.mapFromComputationType(ComputationType.STATE_ESTIMATION)); + assertEquals(OperationType.PCC_MIN, OperationType.mapFromComputationType(ComputationType.PCC_MIN)); + assertEquals(OperationType.SHORT_CIRCUIT, OperationType.mapFromComputationType(ComputationType.SHORT_CIRCUIT)); + assertEquals(OperationType.SHORT_CIRCUIT, OperationType.mapFromComputationType(ComputationType.SHORT_CIRCUIT_ONE_BUS)); + } +} diff --git a/src/test/java/org/gridsuite/study/server/rootnetworks/ModificationToExcludeTest.java b/src/test/java/org/gridsuite/study/server/rootnetworks/ModificationToExcludeTest.java index 818d865f9b..12a58b5a62 100644 --- a/src/test/java/org/gridsuite/study/server/rootnetworks/ModificationToExcludeTest.java +++ b/src/test/java/org/gridsuite/study/server/rootnetworks/ModificationToExcludeTest.java @@ -883,7 +883,7 @@ void testBuildNodeWithExcludedModifications() { rootNetwork0NodeInfo2Entity.setModificationsUuidsToExclude(MODIFICATIONS_TO_EXCLUDE_RN_2); rootNetworkNodeInfoRepository.save(rootNetwork0NodeInfo2Entity); - Mockito.doReturn(Optional.of(3)).when(userAdminService).getUserMaxAllowedBuilds("userId"); + Mockito.doReturn(Map.of(OperationType.BUILD, 3)).when(userAdminService).getUserMaxQuota("userId"); studyService.buildNode(studyEntity.getId(), secondNode.getId(), rootNetworkBasicInfos.getFirst().rootNetworkUuid(), "userId"); ArgumentCaptor buildInfosCaptor = ArgumentCaptor.forClass(BuildInfos.class); diff --git a/src/test/java/org/gridsuite/study/server/service/StudyServiceDynamicSimulationTest.java b/src/test/java/org/gridsuite/study/server/service/StudyServiceDynamicSimulationTest.java index 85ad63e683..52a1c2b6fc 100644 --- a/src/test/java/org/gridsuite/study/server/service/StudyServiceDynamicSimulationTest.java +++ b/src/test/java/org/gridsuite/study/server/service/StudyServiceDynamicSimulationTest.java @@ -67,6 +67,9 @@ class StudyServiceDynamicSimulationTest { @MockitoBean private DynamicSimulationService dynamicSimulationService; + @MockitoBean + private UserAdminService userAdminService; + @Autowired private StudyService studyService; diff --git a/src/test/java/org/gridsuite/study/server/service/UserAdminServiceTest.java b/src/test/java/org/gridsuite/study/server/service/UserAdminServiceTest.java new file mode 100644 index 0000000000..c3456550ff --- /dev/null +++ b/src/test/java/org/gridsuite/study/server/service/UserAdminServiceTest.java @@ -0,0 +1,113 @@ +/** + * 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.study.server.service; + +import org.gridsuite.study.server.ContextConfigurationWithTestChannel; +import org.gridsuite.study.server.dto.OperationType; +import org.gridsuite.study.server.utils.elasticsearch.DisableElasticsearch; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link UserAdminService}, focusing on the operation quota REST calls + * (get max/current quota, start/end operation with quota) introduced alongside {@link OperationType}. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DisableElasticsearch +@ContextConfigurationWithTestChannel +class UserAdminServiceTest { + private static final String USER_ID = "userId"; + + @Autowired + private UserAdminService userAdminService; + + @MockitoBean + private RestTemplate restTemplate; + + @Test + void testGetUserMaxQuota() { + Map expectedQuotas = Map.of(OperationType.LOAD_FLOW, 5, OperationType.BUILD, 10); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + isNull(), + Mockito.>>any())) + .thenReturn(ResponseEntity.ok(expectedQuotas)); + + Map result = userAdminService.getUserMaxQuota(USER_ID); + + assertEquals(expectedQuotas, result); + verify(restTemplate, times(1)).exchange( + matches(".*/users/" + USER_ID + "/quota/max$"), + eq(HttpMethod.GET), + isNull(), + Mockito.>>any()); + } + + @Test + void testGetUserCurrentQuota() { + Map expectedQuotas = Map.of(OperationType.SHORT_CIRCUIT, 1); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + isNull(), + Mockito.>>any())) + .thenReturn(ResponseEntity.ok(expectedQuotas)); + + Map result = userAdminService.getUserCurrentQuota(USER_ID); + + assertEquals(expectedQuotas, result); + verify(restTemplate, times(1)).exchange( + matches(".*/users/" + USER_ID + "/quota/current$"), + eq(HttpMethod.GET), + isNull(), + Mockito.>>any()); + } + + @Test + void testStartOperationWithQuota() { + UUID operationId = UUID.randomUUID(); + + userAdminService.startOperationWithQuota(USER_ID, OperationType.SHORT_CIRCUIT, operationId); + + verify(restTemplate, times(1)).postForEntity( + matches(".*/users/" + USER_ID + "/quota/SHORT_CIRCUIT/" + operationId + "/start$"), + isNull(), + eq(Void.class)); + } + + @Test + void testEndOperationWithQuota() { + UUID operationId = UUID.randomUUID(); + + userAdminService.endOperationWithQuota(USER_ID, OperationType.SHORT_CIRCUIT, operationId); + + verify(restTemplate, times(1)).postForEntity( + matches(".*/users/" + USER_ID + "/quota/SHORT_CIRCUIT/" + operationId + "/end$"), + isNull(), + eq(Void.class)); + } +} diff --git a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java index 86b82ba046..4a3a48d924 100644 --- a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java +++ b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java @@ -26,6 +26,7 @@ import org.gridsuite.study.server.repository.voltageinit.StudyVoltageInitParametersEntity; import org.gridsuite.study.server.service.StudyServerExecutionService; import org.gridsuite.study.server.utils.assertions.Assertions; +import org.gridsuite.study.server.utils.wiremock.UserAdminServerStubs; import org.junit.platform.commons.util.StringUtils; import org.mockito.stubbing.Answer; import org.springframework.cloud.stream.binder.test.OutputDestination; @@ -111,7 +112,10 @@ public static Set getRequestsWithBodyDone(int n, MockWebServer public static void assertRequestMatches(String method, String path, MockWebServer server) { RecordedRequest recordedRequest; try { - recordedRequest = Objects.requireNonNull(server.takeRequest(TIMEOUT, TimeUnit.MILLISECONDS)); + do { + recordedRequest = Objects.requireNonNull(server.takeRequest(TIMEOUT, TimeUnit.MILLISECONDS)); + // skip quota start/end requests that are auto-handled and not part of test assertions + } while (recordedRequest.getPath() != null && recordedRequest.getPath().matches("/v1/users/.*/quota/.*")); } catch (InterruptedException e) { throw new UncheckedInterruptedException(e); } @@ -123,7 +127,12 @@ public static void assertRequestMatches(String method, String path, MockWebServe public static Set getRequestsDone(int n, MockWebServer server) { return IntStream.range(0, n).mapToObj(i -> { try { - return Objects.requireNonNull(server.takeRequest(TIMEOUT, TimeUnit.MILLISECONDS)).getPath(); + RecordedRequest req; + do { + req = Objects.requireNonNull(server.takeRequest(TIMEOUT, TimeUnit.MILLISECONDS)); + // skip quota start/end requests that are auto-handled and not part of test assertions + } while (req.getPath() != null && req.getPath().matches("/v1/users/.*/quota/.*")); + return req.getPath(); } catch (InterruptedException e) { throw new UncheckedInterruptedException(e); } @@ -230,6 +239,9 @@ public static void assertServerRequestsEmptyThenShutdown(MockWebServer server) t public static void assertWiremockServerRequestsEmptyThenShutdown(WireMockServer wireMockServer) throws UncheckedInterruptedException { try { wireMockServer.checkForUnmatchedRequests(); // requests no matched ? (it returns an exception if a request was not matched by wireMock, but does not complain if it was not verified by 'verify') + // Remove quota operation start/end requests that are auto-stubbed and do not need explicit verification + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_START_URL_PATTERN)).build()); + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_END_URL_PATTERN)).build()); var requests = wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())); assertEquals(0, requests.size(), "Unverified WireMock requests found:\n" + requests.stream() .map(r -> "URL: " + r.getUrl() + ", Method: " + r.getMethod() + ", Body: " + r.getBodyAsString() + ", Params: " + r.getQueryParams()) @@ -242,6 +254,9 @@ public static void assertWiremockServerRequestsEmptyThenShutdown(WireMockServer public static void assertWiremockServerRequestsEmptyThenClear(WireMockServer wireMockServer) throws UncheckedInterruptedException { try { wireMockServer.checkForUnmatchedRequests(); + // Remove quota operation start/end requests that are auto-stubbed and do not need explicit verification + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_START_URL_PATTERN)).build()); + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_END_URL_PATTERN)).build()); assertEquals(0, wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())).size()); } finally { wireMockServer.resetAll(); diff --git a/src/test/java/org/gridsuite/study/server/utils/wiremock/UserAdminServerStubs.java b/src/test/java/org/gridsuite/study/server/utils/wiremock/UserAdminServerStubs.java index b2f7b66a87..0fbfeff83a 100644 --- a/src/test/java/org/gridsuite/study/server/utils/wiremock/UserAdminServerStubs.java +++ b/src/test/java/org/gridsuite/study/server/utils/wiremock/UserAdminServerStubs.java @@ -19,10 +19,18 @@ */ public class UserAdminServerStubs { + public static final String QUOTA_START_URL_PATTERN = "/v1/users/.*/quota/.*/start"; + public static final String QUOTA_END_URL_PATTERN = "/v1/users/.*/quota/.*/end"; + private final WireMockServer wireMock; public UserAdminServerStubs(WireMockServer wireMock) { this.wireMock = wireMock; + // Stub the startOperationWithQuota and endOperationWithQuota endpoints used by all computation runs + wireMock.stubFor(WireMock.post(WireMock.urlPathMatching(QUOTA_START_URL_PATTERN)) + .willReturn(WireMock.ok())); + wireMock.stubFor(WireMock.post(WireMock.urlPathMatching(QUOTA_END_URL_PATTERN)) + .willReturn(WireMock.ok())); } public void verifyGetUserProfile(String userId) { diff --git a/src/test/resources/application-default.yml b/src/test/resources/application-default.yml index a723fb62d4..abd5a9a4d1 100644 --- a/src/test/resources/application-default.yml +++ b/src/test/resources/application-default.yml @@ -20,5 +20,7 @@ powsybl-ws: query: ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE hostPort: ":" +study: + enable-operation-quotas: false