Add (error)simulations to DMI Stub ProvMnS Interface 75/142775/4 master
authorToineSiebelink <toine.siebelink@est.tech>
Tue, 16 Dec 2025 17:36:52 +0000 (17:36 +0000)
committerToineSiebelink <toine.siebelink@est.tech>
Wed, 17 Dec 2025 15:17:26 +0000 (15:17 +0000)
- Added Controller Test for easier testing of simulation URI patterns
  (faster then build & PostMan!)
- Add Validator Dependency for latest Springboot
- Add timeout and http error simulation based on patterns in FDN
- Add info level logging to all stub methods
  (this might affect k6 performance tests!)
- Changed ProvMns interface to allow (any) object be returned for all operation incl. Delete
  (inline with production interfaces updates)

Issue-ID: CPS-3094
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
Change-Id: I25d1c660d72bbd2ab929cd66d65e304e6255637c

dmi-stub/dmi-stub-app/pom.xml
dmi-stub/dmi-stub-app/src/test/groovy/org/onap/cps/ncmp/dmi/rest/stub/controller/ProvMnsStubControllerSpec.groovy [new file with mode: 0644]
dmi-stub/dmi-stub-service/pom.xml
dmi-stub/dmi-stub-service/src/main/java/org/onap/cps/ncmp/dmi/provmns/api/ProvMnS.java
dmi-stub/dmi-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/controller/DmiRestStubController.java
dmi-stub/dmi-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/controller/ProvMnsStubController.java
dmi-stub/dmi-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/utils/Sleeper.java [new file with mode: 0644]

index ae293d9..f1f8e9b 100644 (file)
                 </exclusion>
             </exclusions>
         </dependency>
                 </exclusion>
             </exclusions>
         </dependency>
+        <!-- T E S T   D E P E N D E N C I E S -->
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-spring</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-json</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
     </dependencies>
 
     <build>
         </snapshotRepository>
     </distributionManagement>
 
         </snapshotRepository>
     </distributionManagement>
 
-</project>
\ No newline at end of file
+</project>
diff --git a/dmi-stub/dmi-stub-app/src/test/groovy/org/onap/cps/ncmp/dmi/rest/stub/controller/ProvMnsStubControllerSpec.groovy b/dmi-stub/dmi-stub-app/src/test/groovy/org/onap/cps/ncmp/dmi/rest/stub/controller/ProvMnsStubControllerSpec.groovy
new file mode 100644 (file)
index 0000000..c93d5a9
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.dmi.rest.stub.controller
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.ncmp.dmi.provmns.model.ResourceOneOf
+import org.onap.cps.ncmp.dmi.rest.stub.utils.Sleeper
+import org.spockframework.spring.SpringBean
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.validation.Validator
+import spock.lang.Specification
+
+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.patch
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
+
+@WebMvcTest(controllers = ProvMnsStubController)
+class ProvMnsStubControllerSpec extends Specification {
+
+    @Autowired
+    MockMvc mockMvc
+
+    @SpringBean
+    Sleeper sleeper = Mock()
+
+    @Autowired
+    ObjectMapper objectMapper
+
+    @SpringBean
+    Validator validator = Mock()
+
+    def 'ProvMnS GET request with #scenario.'() {
+        given: 'url'
+            def url = "/ProvMnS/v1/someSegment=1/otherSegment=2/${segmentName}=${segmentValue}/finalSegment=3"
+        when: 'get request is executed'
+            def response = mockMvc.perform(get(url)).andReturn().response
+        then: 'response status is #expectedHttpStatus'
+            assert response.status == expectedHttpStatus.value()
+        and: 'content contains the expected json (snippet)'
+            assert response.getContentAsString().contains(expectedContentSnippet)
+        where: 'following simulations are applied'
+            scenario        | segmentName      | segmentValue     || expectedHttpStatus       || expectedContentSnippet
+            'no simulation' | 'anotherSegment' | 'some value'     || HttpStatus.OK            || '"objectClass":"dummyClass"'
+            'delay 1'       | 'dmiSimulation'  | 'slowResponse_1' || HttpStatus.OK            || '"objectClass":"dummyClass"'
+            'http error'    | 'dmiSimulation'  | 'httpError_418'  || HttpStatus.I_AM_A_TEAPOT || '"status":"418"'
+    }
+
+    def 'ProvMnS PUT request with #scenario.'() {
+        given: 'url and some resource as body'
+            def url = "/ProvMnS/v1/someSegment=1/otherSegment=2/${segmentName}=${segmentValue}/finalSegment=3"
+            def requestBody = objectMapper.writeValueAsString(new ResourceOneOf('myId'))
+        when: 'put request is executed'
+            def response = mockMvc.perform(put(url)
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(requestBody))
+                .andReturn().response
+        then: 'response status is #expectedHttpStatus'
+            assert response.status == expectedHttpStatus.value()
+        and: 'content contains the expected json (snippet)'
+            assert response.getContentAsString().contains(expectedContentSnippet)
+        where: 'following simulations are applied'
+            scenario        | segmentName      | segmentValue     || expectedHttpStatus       || expectedContentSnippet
+            'no simulation' | 'anotherSegment' | 'some value'     || HttpStatus.OK            || '"id":"myId"'
+            'delay 1'       | 'dmiSimulation'  | 'slowResponse_1' || HttpStatus.OK            || '"id":"myId"'
+            'http error'    | 'dmiSimulation'  | 'httpError_418'  || HttpStatus.I_AM_A_TEAPOT || '"status":"418"'
+    }
+
+    def 'ProvMnS PATCH request with #scenario.'() {
+        given: 'url and some resource as body'
+            def url = "/ProvMnS/v1/someSegment=1/otherSegment=2/${segmentName}=${segmentValue}/finalSegment=3"
+            def requestBody = objectMapper.writeValueAsString(new ResourceOneOf('myId'))
+        when: 'patch request is executed'
+            def response = mockMvc.perform(patch(url)
+                .contentType("application/json-patch+json")
+                .content(requestBody))
+                .andReturn().response
+        then: 'response status is #expectedHttpStatus'
+            assert response.status == expectedHttpStatus.value()
+        and: 'content contains the expected json (snippet)'
+            assert response.getContentAsString().contains(expectedContentSnippet)
+        where: 'following simulations are applied'
+            scenario        | segmentName      | segmentValue     || expectedHttpStatus       || expectedContentSnippet
+            'no simulation' | 'anotherSegment' | 'some value'     || HttpStatus.OK            || '"id":"myId"'
+            'delay 1'       | 'dmiSimulation'  | 'slowResponse_1' || HttpStatus.OK            || '"id":"myId"'
+            'http error'    | 'dmiSimulation'  | 'httpError_418'  || HttpStatus.I_AM_A_TEAPOT || '"status":"418"'
+    }
+
+    def 'ProvMnS DELETE request with #scenario.'() {
+        given: 'url'
+            def url = "/ProvMnS/v1/someSegment=1/otherSegment=2/${segmentName}=${segmentValue}/finalSegment=3"
+        when: 'delete request is executed'
+            def response = mockMvc.perform(delete(url)).andReturn().response
+        then: 'response status is #expectedHttpStatus'
+            assert response.status == expectedHttpStatus.value()
+        and: 'content only contains anything in case of an http error'
+            assert response.getContentAsString().contains(expectedContentSnippet)
+        where: 'following simulations are applied'
+            scenario        | segmentName      | segmentValue     || expectedHttpStatus       || expectedContentSnippet
+            'no simulation' | 'anotherSegment' | 'some value'     || HttpStatus.OK            || ''
+            'delay 1'       | 'dmiSimulation'  | 'slowResponse_1' || HttpStatus.OK            || ''
+            'http error'    | 'dmiSimulation'  | 'httpError_418'  || HttpStatus.I_AM_A_TEAPOT || '"status":"418"'
+    }
+
+}
index a8d1abd..58d5d82 100644 (file)
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.googlecode.json-simple</groupId>
             <artifactId>json-simple</artifactId>
         <dependency>
             <groupId>com.googlecode.json-simple</groupId>
             <artifactId>json-simple</artifactId>
             <artifactId>groovy-json</artifactId>
             <scope>test</scope>
         </dependency>
             <artifactId>groovy-json</artifactId>
             <scope>test</scope>
         </dependency>
-
     </dependencies>
     <build>
         <plugins>
     </dependencies>
     <build>
         <plugins>
             </plugin>
         </plugins>
     </build>
             </plugin>
         </plugins>
     </build>
-</project>
\ No newline at end of file
+</project>
index 4ac2151..68a5b3d 100644 (file)
@@ -74,7 +74,7 @@ public interface ProvMnS {
         value = "v1/**",
         produces = { "application/json" }
     )
         value = "v1/**",
         produces = { "application/json" }
     )
-    ResponseEntity<Void> deleteMoi(HttpServletRequest httpServletRequest);
+    ResponseEntity<Object> deleteMoi(HttpServletRequest httpServletRequest);
 
 
     /**
 
 
     /**
@@ -127,7 +127,7 @@ public interface ProvMnS {
         produces = { "application/json"}
     )
 
         produces = { "application/json"}
     )
 
-    ResponseEntity<Resource> getMoi(
+    ResponseEntity<Object> getMoi(
         HttpServletRequest httpServletRequest,
         @Parameter(name = "scope", description = "This parameter extends the set of targeted resources beyond the "
             + "base resource identified with the path component of the URI. "
         HttpServletRequest httpServletRequest,
         @Parameter(name = "scope", description = "This parameter extends the set of targeted resources beyond the "
             + "base resource identified with the path component of the URI. "
@@ -199,7 +199,7 @@ public interface ProvMnS {
         consumes = {"application/json-patch+json", "application/3gpp-json-patch+json" }
     )
 
         consumes = {"application/json-patch+json", "application/3gpp-json-patch+json" }
     )
 
-    ResponseEntity<Resource> patchMoi(
+    ResponseEntity<Object> patchMoi(
         HttpServletRequest httpServletRequest,
         @Parameter(name = "Resource", description = "The request body describes changes to be made to the target "
             + "resources. The following patch media types are available   "
         HttpServletRequest httpServletRequest,
         @Parameter(name = "Resource", description = "The request body describes changes to be made to the target "
             + "resources. The following patch media types are available   "
@@ -263,7 +263,7 @@ public interface ProvMnS {
         consumes = { "application/json" }
     )
 
         consumes = { "application/json" }
     )
 
-    ResponseEntity<Resource> putMoi(
+    ResponseEntity<Object> putMoi(
         HttpServletRequest httpServletRequest,
         @Parameter(name = "Resource", description = "", required = true) @Valid @RequestBody Resource resource
     );
         HttpServletRequest httpServletRequest,
         @Parameter(name = "Resource", description = "", required = true) @Valid @RequestBody Resource resource
     );
index 33975b0..9e26d80 100644 (file)
@@ -110,6 +110,7 @@ public class DmiRestStubController {
      */
     @PostMapping("/v1/tagMapping")
     public ResponseEntity<Map<String, String>> addTagForMapping(@RequestBody final Map<String, String> requestBody) {
      */
     @PostMapping("/v1/tagMapping")
     public ResponseEntity<Map<String, String>> addTagForMapping(@RequestBody final Map<String, String> requestBody) {
+        log.info("tagMapping: add {}", requestBody);
         moduleSetTagPerCmHandleId.putAll(requestBody);
         return new ResponseEntity<>(requestBody, HttpStatus.CREATED);
     }
         moduleSetTagPerCmHandleId.putAll(requestBody);
         return new ResponseEntity<>(requestBody, HttpStatus.CREATED);
     }
@@ -120,7 +121,8 @@ public class DmiRestStubController {
      * @return The map represents the module set tag mapping.
      */
     @GetMapping("/v1/tagMapping")
      * @return The map represents the module set tag mapping.
      */
     @GetMapping("/v1/tagMapping")
-    public ResponseEntity<Map<String, String>> getTagMapping() {
+    public ResponseEntity<Map<String, String>> getTagMapping()    {
+        log.info("tagMapping: get");
         return ResponseEntity.ok(moduleSetTagPerCmHandleId);
     }
 
         return ResponseEntity.ok(moduleSetTagPerCmHandleId);
     }
 
@@ -131,6 +133,7 @@ public class DmiRestStubController {
      */
     @GetMapping("/v1/tagMapping/ch/{cmHandleId}")
     public ResponseEntity<String> getTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
      */
     @GetMapping("/v1/tagMapping/ch/{cmHandleId}")
     public ResponseEntity<String> getTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
+        log.info("tagMapping: get cm handle id: {}", cmHandleId);
         return ResponseEntity.ok(moduleSetTagPerCmHandleId.get(cmHandleId));
     }
 
         return ResponseEntity.ok(moduleSetTagPerCmHandleId.get(cmHandleId));
     }
 
@@ -145,6 +148,7 @@ public class DmiRestStubController {
 
     @PutMapping("/v1/tagMapping")
     public ResponseEntity<Map<String, String>> updateTagMapping(@RequestBody final Map<String, String> requestBody) {
 
     @PutMapping("/v1/tagMapping")
     public ResponseEntity<Map<String, String>> updateTagMapping(@RequestBody final Map<String, String> requestBody) {
+        log.info("tagMapping: update: {}", requestBody);
         moduleSetTagPerCmHandleId.putAll(requestBody);
         return ResponseEntity.noContent().build();
     }
         moduleSetTagPerCmHandleId.putAll(requestBody);
         return ResponseEntity.noContent().build();
     }
@@ -157,6 +161,7 @@ public class DmiRestStubController {
      */
     @DeleteMapping("/v1/tagMapping/ch/{cmHandleId}")
     public ResponseEntity<String> deleteTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
      */
     @DeleteMapping("/v1/tagMapping/ch/{cmHandleId}")
     public ResponseEntity<String> deleteTagMappingByCmHandleId(@PathVariable final String cmHandleId) {
+        log.info("tagMapping: remove cm handle id: {}", cmHandleId);
         moduleSetTagPerCmHandleId.remove(cmHandleId);
         return ResponseEntity.ok(String.format("Mapping of %s is deleted successfully", cmHandleId));
     }
         moduleSetTagPerCmHandleId.remove(cmHandleId);
         return ResponseEntity.ok(String.format("Mapping of %s is deleted successfully", cmHandleId));
     }
@@ -173,6 +178,7 @@ public class DmiRestStubController {
     @ModuleInitialProcess
     public ResponseEntity<String> getModuleReferences(@PathVariable("cmHandleId") final String cmHandleId,
                                                       @RequestBody final Object moduleReferencesRequest) {
     @ModuleInitialProcess
     public ResponseEntity<String> getModuleReferences(@PathVariable("cmHandleId") final String cmHandleId,
                                                       @RequestBody final Object moduleReferencesRequest) {
+        log.info("get module references for cm handle id: {}", cmHandleId);
         return processModuleRequest(moduleReferencesRequest, MODULE_REFERENCE_RESPONSE, moduleReferencesDelayMs);
     }
 
         return processModuleRequest(moduleReferencesRequest, MODULE_REFERENCE_RESPONSE, moduleReferencesDelayMs);
     }
 
@@ -189,6 +195,7 @@ public class DmiRestStubController {
     public ResponseEntity<String> getModuleResources(
             @PathVariable("cmHandleId") final String cmHandleId,
             @RequestBody final Object moduleResourcesReadRequest) {
     public ResponseEntity<String> getModuleResources(
             @PathVariable("cmHandleId") final String cmHandleId,
             @RequestBody final Object moduleResourcesReadRequest) {
+        log.info("get module resources for cm handle id: {}", cmHandleId);
         return processModuleRequest(moduleResourcesReadRequest, MODULE_RESOURCE_RESPONSE, moduleResourcesDelayMs);
     }
 
         return processModuleRequest(moduleResourcesReadRequest, MODULE_RESOURCE_RESPONSE, moduleResourcesDelayMs);
     }
 
@@ -212,6 +219,7 @@ public class DmiRestStubController {
             @RequestParam(value = "topic", required = false) final String topic,
             @RequestHeader(value = "Authorization", required = false) final String authorization,
             @RequestBody final String requestBody) {
             @RequestParam(value = "topic", required = false) final String topic,
             @RequestHeader(value = "Authorization", required = false) final String authorization,
             @RequestBody final String requestBody) {
+        log.info("create resource data for cm handle id: {}", cmHandleId);
         log.debug("DMI AUTH HEADER: {}", authorization);
         final String passthroughOperationType = getPassthroughOperationType(requestBody);
         if (passthroughOperationType.equals("read")) {
         log.debug("DMI AUTH HEADER: {}", authorization);
         final String passthroughOperationType = getPassthroughOperationType(requestBody);
         if (passthroughOperationType.equals("read")) {
@@ -241,7 +249,7 @@ public class DmiRestStubController {
             @RequestBody final DmiDataOperationRequest dmiDataOperationRequest) {
         delay(writeDataForCmHandleDelayMs);
         try {
             @RequestBody final DmiDataOperationRequest dmiDataOperationRequest) {
         delay(writeDataForCmHandleDelayMs);
         try {
-            log.debug("Request received from the NCMP to DMI Plugin: {}",
+            log.info("Request received from the NCMP to DMI Plugin: {}",
                     objectMapper.writeValueAsString(dmiDataOperationRequest));
         } catch (final JsonProcessingException jsonProcessingException) {
             log.warn("Unable to process dmi data operation request to json string");
                     objectMapper.writeValueAsString(dmiDataOperationRequest));
         } catch (final JsonProcessingException jsonProcessingException) {
             log.warn("Unable to process dmi data operation request to json string");
@@ -269,8 +277,9 @@ public class DmiRestStubController {
     public ResponseEntity<SubjobWriteResponse> consumeWriteSubJobs(
                                                         @RequestBody final SubjobWriteRequest subJobWriteRequest,
                                                         @RequestParam("destination") final String destination) {
     public ResponseEntity<SubjobWriteResponse> consumeWriteSubJobs(
                                                         @RequestBody final SubjobWriteRequest subJobWriteRequest,
                                                         @RequestParam("destination") final String destination) {
-        log.debug("Destination: {}", destination);
-        log.debug("Request body: {}", subJobWriteRequest);
+        log.info("cm write (datajob) request");
+        log.info("Destination: {}", destination);
+        log.info("Request body: {}", subJobWriteRequest);
         return ResponseEntity.ok(new SubjobWriteResponse(String.valueOf(subJobWriteRequestCounter.incrementAndGet()),
                 "some-dmi-service-name", "my-data-producer-id"));
     }
         return ResponseEntity.ok(new SubjobWriteResponse(String.valueOf(subJobWriteRequestCounter.incrementAndGet()),
                 "some-dmi-service-name", "my-data-producer-id"));
     }
@@ -287,7 +296,7 @@ public class DmiRestStubController {
     public ResponseEntity<Map<String, String>> retrieveDataJobStatus(
             @PathVariable("dataProducerId") final String dataProducerId,
             @PathVariable("dataProducerJobId") final String dataProducerJobId) {
     public ResponseEntity<Map<String, String>> retrieveDataJobStatus(
             @PathVariable("dataProducerId") final String dataProducerId,
             @PathVariable("dataProducerJobId") final String dataProducerJobId) {
-        log.debug("Received request to retrieve data job status. Request ID: {}, Data Producer Job ID: {}",
+        log.info("Received request to retrieve data job status. Request ID: {}, Data Producer Job ID: {}",
                 dataProducerId, dataProducerJobId);
         return ResponseEntity.ok(Map.of("status", "FINISHED"));
     }
                 dataProducerId, dataProducerJobId);
         return ResponseEntity.ok(Map.of("status", "FINISHED"));
     }
@@ -305,7 +314,7 @@ public class DmiRestStubController {
             @PathVariable("dataProducerId") final String dataProducerId,
             @PathVariable("dataProducerJobId") final String dataProducerJobId,
             @RequestParam(name = "destination") final String destination) {
             @PathVariable("dataProducerId") final String dataProducerId,
             @PathVariable("dataProducerJobId") final String dataProducerJobId,
             @RequestParam(name = "destination") final String destination) {
-        log.debug("Received request to retrieve data job result. Data Producer ID: {}, "
+        log.info("Received request to retrieve data job result. Data Producer ID: {}, "
                         + "Data Producer Job ID: {}, Destination: {}",
                 dataProducerId, dataProducerJobId, destination);
         return ResponseEntity.ok(Map.of("result", "some status"));
                         + "Data Producer Job ID: {}, Destination: {}",
                 dataProducerId, dataProducerJobId, destination);
         return ResponseEntity.ok(Map.of("result", "some status"));
index 435f1eb..25fb51c 100644 (file)
 
 package org.onap.cps.ncmp.dmi.rest.stub.controller;
 
 
 package org.onap.cps.ncmp.dmi.rest.stub.controller;
 
-
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletRequest;
+import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.dmi.provmns.api.ProvMnS;
 import org.onap.cps.ncmp.dmi.provmns.model.ClassNameIdGetDataNodeSelectorParameter;
 import org.onap.cps.ncmp.dmi.provmns.api.ProvMnS;
 import org.onap.cps.ncmp.dmi.provmns.model.ClassNameIdGetDataNodeSelectorParameter;
+import org.onap.cps.ncmp.dmi.provmns.model.ErrorResponseDefault;
 import org.onap.cps.ncmp.dmi.provmns.model.Resource;
 import org.onap.cps.ncmp.dmi.provmns.model.ResourceOneOf;
 import org.onap.cps.ncmp.dmi.provmns.model.Scope;
 import org.onap.cps.ncmp.dmi.provmns.model.Resource;
 import org.onap.cps.ncmp.dmi.provmns.model.ResourceOneOf;
 import org.onap.cps.ncmp.dmi.provmns.model.Scope;
+import org.onap.cps.ncmp.dmi.rest.stub.utils.Sleeper;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -37,19 +43,36 @@ import org.springframework.web.bind.annotation.RestController;
 @RestController
 @RequestMapping("${rest.api.provmns-base-path}")
 @RequiredArgsConstructor
 @RestController
 @RequestMapping("${rest.api.provmns-base-path}")
 @RequiredArgsConstructor
+@Slf4j
 public class ProvMnsStubController implements ProvMnS {
 
 public class ProvMnsStubController implements ProvMnS {
 
+    static final ResourceOneOf dummyResource = new ResourceOneOf("some id");
+
+    static final Pattern PATTERN_SIMULATION = Pattern.compile("dmiSimulation=(\\w+_\\d{1,3})");
+    static final Pattern PATTERN_HTTP_ERROR = Pattern.compile("httpError_(\\d{3})");
+    static final Pattern PATTERN_SLOW_RESPONSE = Pattern.compile("slowResponse_(\\d{1,3})");
+
+    private final Sleeper sleeper;
+
+    static {
+        dummyResource.setObjectClass("dummyClass");
+        dummyResource.setObjectInstance("dummyInstance");
+        dummyResource.setAttributes(Collections.singletonMap("dummyAttribute", "dummy value"));
+    }
+
     /**
      * Replaces a complete single resource or creates it if it does not exist.
      *
      * @param httpServletRequest      URI request including path
      * @param resource                Resource representation of the resource to be created or replaced
     /**
      * Replaces a complete single resource or creates it if it does not exist.
      *
      * @param httpServletRequest      URI request including path
      * @param resource                Resource representation of the resource to be created or replaced
-     * @return {@code ResponseEntity} The representation of the updated resource is returned in the response
+     * @return {@code Object}         The representation of the updated resource is returned in the response
      *                                message body.
      */
     @Override
      *                                message body.
      */
     @Override
-    public ResponseEntity<Resource> putMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
-        return new ResponseEntity<>(resource, HttpStatus.OK);
+    public ResponseEntity<Object> putMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
+        log.info("putMoi: {}", resource);
+        final Optional<ResponseEntity<Object>> optionalResponseEntity = simulate(httpServletRequest);
+        return optionalResponseEntity.orElseGet(() -> new ResponseEntity<>(resource, HttpStatus.OK));
     }
 
     /**
     }
 
     /**
@@ -73,11 +96,14 @@ public class ProvMnsStubController implements ProvMnS {
      *                                in the response message body.
      */
     @Override
      *                                in the response message body.
      */
     @Override
-    public ResponseEntity<Resource> getMoi(final HttpServletRequest httpServletRequest, final Scope scope,
+    public ResponseEntity<Object> getMoi(final HttpServletRequest httpServletRequest, final Scope scope,
                                            final String filter, final List<String> attributes,
                                            final List<String> fields,
                                            final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) {
                                            final String filter, final List<String> attributes,
                                            final List<String> fields,
                                            final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) {
-        return new ResponseEntity<>(new ResourceOneOf("exampleResourceId"), HttpStatus.OK);
+        log.info("getMoi: scope: {}, filter: {}, attributes: {}, fields: {}, dataNodeSelector: {}",
+                scope, filter, attributes, fields, dataNodeSelector);
+        final Optional<ResponseEntity<Object>> optionalResponseEntity = simulate(httpServletRequest);
+        return optionalResponseEntity.orElseGet(() -> new ResponseEntity<>(dummyResource, HttpStatus.OK));
     }
 
     /**
     }
 
     /**
@@ -88,8 +114,10 @@ public class ProvMnsStubController implements ProvMnS {
      * @return {@code ResponseEntity} The updated resource representations are returned in the response message body.
      */
     @Override
      * @return {@code ResponseEntity} The updated resource representations are returned in the response message body.
      */
     @Override
-    public ResponseEntity<Resource> patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
-        return new ResponseEntity<>(resource, HttpStatus.OK);
+    public ResponseEntity<Object> patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
+        log.info("patchMoi: {}", resource);
+        final Optional<ResponseEntity<Object>> optionalResponseEntity = simulate(httpServletRequest);
+        return optionalResponseEntity.orElseGet(() -> new ResponseEntity<>(resource, HttpStatus.OK));
     }
 
     /**
     }
 
     /**
@@ -99,7 +127,44 @@ public class ProvMnsStubController implements ProvMnS {
      * @return {@code ResponseEntity} The response body is empty, HTTP status returned.
      */
     @Override
      * @return {@code ResponseEntity} The response body is empty, HTTP status returned.
      */
     @Override
-    public ResponseEntity<Void> deleteMoi(final HttpServletRequest httpServletRequest) {
-        return new ResponseEntity<>(HttpStatus.OK);
+    public ResponseEntity<Object> deleteMoi(final HttpServletRequest httpServletRequest) {
+        log.info("deleteMoi:");
+        final Optional<ResponseEntity<Object>> optionalResponseEntity = simulate(httpServletRequest);
+        return optionalResponseEntity.orElseGet(() -> new ResponseEntity<>(HttpStatus.OK));
     }
     }
+
+    private Optional<ResponseEntity<Object>> simulate(final HttpServletRequest httpServletRequest) {
+        Matcher matcher = PATTERN_SIMULATION.matcher(httpServletRequest.getRequestURI());
+        if (matcher.find()) {
+            final String simulation = matcher.group(1);
+            matcher = PATTERN_SLOW_RESPONSE.matcher(simulation);
+            if (matcher.matches()) {
+                haveALittleRest(Integer.parseInt(matcher.group(1)));
+            }
+            matcher = PATTERN_HTTP_ERROR.matcher(simulation);
+            if (matcher.matches()) {
+                return Optional.of(createErrorRsponseEntity(Integer.parseInt(matcher.group(1))));
+            }
+        }
+        return Optional.empty();
+    }
+
+    private void haveALittleRest(final int durationInSeconds) {
+        log.warn("Stub is mocking slow response; delay {} seconds", durationInSeconds);
+        try {
+            sleeper.haveALittleRest(durationInSeconds);
+        } catch (final InterruptedException e) {
+            log.trace("Sleep interrupted, re-interrupting the thread");
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    private static ResponseEntity<Object> createErrorRsponseEntity(final int errorCode) {
+        log.warn("Stub is mocking an error response, code: {}", errorCode);
+        final ErrorResponseDefault errorResponseDefault = new ErrorResponseDefault("ERROR_FROM_STUB");
+        errorResponseDefault.setTitle("Title set by Stub");
+        errorResponseDefault.setStatus(String.valueOf(errorCode));
+        return new ResponseEntity<>(errorResponseDefault, HttpStatus.valueOf(errorCode));
+    }
+
 }
 }
diff --git a/dmi-stub/dmi-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/utils/Sleeper.java b/dmi-stub/dmi-stub-service/src/main/java/org/onap/cps/ncmp/dmi/rest/stub/utils/Sleeper.java
new file mode 100644 (file)
index 0000000..c100e31
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 OpenInfra Foundation Europe
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.dmi.rest.stub.utils;
+
+import java.util.concurrent.TimeUnit;
+import org.springframework.stereotype.Component;
+
+/**
+ * This class is to extract out sleep functionality so the interrupted exception handling can
+ * be covered with a test (e.g. using spy on Sleeper) and help to get too 100% code coverage.
+ */
+@Component
+public class Sleeper {
+    public void haveALittleRest(final int durationInSeconds) throws InterruptedException {
+        TimeUnit.SECONDS.sleep(durationInSeconds);
+    }
+}