2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021 Pantheon.tech
4 * Modifications Copyright (C) 2021 highstreet technologies GmbH
5 * Modifications Copyright (C) 2021-2023 Nordix Foundation
6 * Modifications Copyright (C) 2021-2022 Bell Canada.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
20 * SPDX-License-Identifier: Apache-2.0
21 * ============LICENSE_END=========================================================
24 package org.onap.cps.ncmp.rest.controller
26 import com.fasterxml.jackson.databind.ObjectMapper
27 import org.mapstruct.factory.Mappers
28 import org.onap.cps.TestUtils
29 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
30 import org.onap.cps.ncmp.api.NetworkCmProxyQueryService
31 import org.onap.cps.ncmp.api.inventory.CmHandleState
32 import org.onap.cps.ncmp.api.inventory.CompositeState
33 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
34 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
35 import org.onap.cps.ncmp.rest.model.BatchOperationDefinition
36 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
37 import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler
38 import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler
39 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
40 import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper
41 import org.onap.cps.ncmp.rest.mapper.ResourceDataBatchRequestMapper
42 import org.onap.cps.ncmp.rest.model.ResourceDataBatchRequest
43 import org.onap.cps.ncmp.rest.util.DeprecationHelper
44 import org.onap.cps.spi.FetchDescendantsOption
45 import org.onap.cps.spi.model.ModuleDefinition
46 import org.onap.cps.spi.model.ModuleReference
47 import org.onap.cps.utils.JsonObjectMapper
48 import org.spockframework.spring.SpringBean
49 import org.springframework.beans.factory.annotation.Autowired
50 import org.springframework.beans.factory.annotation.Value
51 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
52 import org.springframework.http.HttpStatus
53 import org.springframework.http.MediaType
54 import org.springframework.test.web.servlet.MockMvc
55 import spock.lang.Shared
56 import spock.lang.Specification
57 import java.time.OffsetDateTime
58 import java.time.ZoneOffset
59 import java.time.format.DateTimeFormatter
61 import static org.onap.cps.ncmp.api.inventory.CompositeState.DataStores
62 import static org.onap.cps.ncmp.api.inventory.CompositeState.Operational
63 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
64 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
65 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
66 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
67 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
68 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
69 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
70 import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH
71 import static org.onap.cps.ncmp.api.impl.operations.OperationType.DELETE
72 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
73 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
74 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
75 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
76 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
79 @WebMvcTest(NetworkCmProxyController)
80 class NetworkCmProxyControllerSpec extends Specification {
86 NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
89 NetworkCmProxyQueryService mockNetworkCmProxyQueryService = Mock()
92 ObjectMapper objectMapper = new ObjectMapper()
95 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
98 NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
101 CmHandleStateMapper cmHandleStateMapper = Mappers.getMapper(CmHandleStateMapper)
104 ResourceDataBatchRequestMapper resourceDataBatchRequestMapper = Mappers.getMapper(ResourceDataBatchRequestMapper)
107 CpsNcmpTaskExecutor spiedCpsTaskExecutor = Spy()
110 DeprecationHelper stubbedDeprecationHelper = Stub()
113 NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler = new NcmpCachedResourceRequestHandler(spiedCpsTaskExecutor, mockNetworkCmProxyDataService, mockNetworkCmProxyQueryService)
116 NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler = new NcmpPassthroughResourceRequestHandler(spiedCpsTaskExecutor, mockNetworkCmProxyDataService)
118 @Value('${rest.api.ncmp-base-path}/v1')
121 def requestBody = '{"some-key":"some-value"}'
123 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
127 def NO_REQUEST_ID = null
128 def TIMOUT_FOR_TEST = 1234
131 ncmpCachedResourceRequestHandler.notificationFeatureEnabled = true
132 ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
133 ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = true
134 ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
137 def 'Get Resource Data from pass-through operational.'() {
138 given: 'resource data url'
139 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
140 "?resourceIdentifier=parent/child&options=(a=1,b=2)"
141 when: 'get data resource request is performed'
142 def response = mvc.perform(
144 .contentType(MediaType.APPLICATION_JSON)
145 ).andReturn().response
146 then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle'
147 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle',
148 'parent/child','(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID)
149 and: 'response status is Ok'
150 response.status == HttpStatus.OK.value()
153 def 'Get Resource Data Async Topic Handling with #scenario.'() {
154 given: 'resource data url'
155 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational?resourceIdentifier=parent/child&${topicQueryParam}"
156 when: 'get data resource request is performed'
157 def response = mvc.perform(
158 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
159 then: 'task executor is called appropriate number of times'
160 expectedNumberOfTaskExecutions * spiedCpsTaskExecutor.executeTask(_, TIMOUT_FOR_TEST)
161 and: 'response status is OK'
162 response.status == HttpStatus.OK.value()
163 where: 'the following parameters are used'
164 scenario | datastoreInUrl | topicQueryParam || expectedNumberOfTaskExecutions
165 'url with valid topic' | 'passthrough-operational' | '&topic=my-topic-name' || 1
166 'no topic in url' | 'passthrough-operational' | '' || 0
167 'null topic in url' | 'passthrough-operational' | '&topic=null' || 1
170 def 'Get Resource Data from ncmp-datastore:operational (cached) parameters handling with #scenario.'() {
171 given: 'resource data url'
172 def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational" +
173 "?resourceIdentifier=parent/child${additionalUrlParam}"
174 when: 'get data resource request is performed'
175 def response = mvc.perform(
176 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
177 then: 'task executor is called appropriate number of times'
178 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle('ncmp-datastore:operational', 'h123', 'parent/child', expectedIncludeDescendants)
179 and: 'response status is OK'
180 response.status == HttpStatus.OK.value()
181 where: 'the following parameters are used'
182 scenario | additionalUrlParam || expectedIncludeDescendants
183 'no additional param' | '' || OMIT_DESCENDANTS
184 'include descendants true' | '&include-descendants=true' || INCLUDE_ALL_DESCENDANTS
185 'include descendants TRUE' | '&include-descendants=true' || INCLUDE_ALL_DESCENDANTS
186 'include descendants false' | '&include-descendants=false' || OMIT_DESCENDANTS
187 'include descendants FALSE' | '&include-descendants=FALSE' || OMIT_DESCENDANTS
188 'options (ignored)' | '&options=(a-=1)' || OMIT_DESCENDANTS
191 def 'Get Resource Data with invalid topic parameter: #scenario.'() {
192 given: 'resource data url'
193 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
194 "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
195 when: 'get data resource (async) request is performed'
196 def response = mvc.perform(
197 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
198 then: 'abad request is returned'
199 response.status == HttpStatus.BAD_REQUEST.value()
200 where: 'the following parameters are used'
201 scenario | datastoreInUrl | topicQueryParam
202 'empty topic in url' | 'passthrough-operational' | '&topic=\"\"'
203 'missing topic in url' | 'passthrough-operational' | '&topic='
204 'blank topic value in url' | 'passthrough-operational' | '&topic=\" \"'
205 'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#'
208 def 'Get (async) batch resource data from dmi service.'() {
209 given: 'batch resource data url'
210 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
211 def resourceDataBatchRequestJsonData = jsonObjectMapper.asJsonString(
212 getResourceDataBatchRequest("read", datastore.datastoreName))
213 def expectedDmiResourceDataBatchRequest
214 = jsonObjectMapper.convertJsonString(resourceDataBatchRequestJsonData, org.onap.cps.ncmp.api.models.ResourceDataBatchRequest.class)
215 when: 'post data resource request is performed'
216 def response = mvc.perform(
218 .contentType(MediaType.APPLICATION_JSON)
219 .content(resourceDataBatchRequestJsonData)
220 ).andReturn().response
221 then: 'response status is Ok'
222 response.status == HttpStatus.OK.value()
223 and: 'async request id is generated'
224 assert response.contentAsString.contains("requestId")
225 then: 'wait a little to allow execution of service method by task executor (on separate thread)'
227 then: 'the service has been invoked with the correct parameters '
228 1 * mockNetworkCmProxyDataService.requestResourceDataForCmHandleBatch('my-topic-name', expectedDmiResourceDataBatchRequest, _)
229 where: 'the following data stores are used'
230 datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
233 def 'Get batch resource data for #scenario from dmi service.'() {
234 given: 'batch resource data url'
235 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
236 def resourceDataBatchRequestJsonData = jsonObjectMapper.asJsonString(
237 getResourceDataBatchRequest(operation, datastore))
238 when: 'post data resource request is performed'
239 def response = mvc.perform(
241 .contentType(MediaType.APPLICATION_JSON)
242 .content(resourceDataBatchRequestJsonData)
243 ).andReturn().response
244 then: 'response status is BAD_REQUEST'
245 response.status == HttpStatus.BAD_REQUEST.value()
246 where: 'the following parameters are used'
247 scenario | datastore | operation
248 'non-supported datastoreName' | OPERATIONAL.datastoreName | 'read'
249 'non-supported operation (passthrough-running)' | PASSTHROUGH_RUNNING.datastoreName | 'create'
250 'non-supported operation (passthrough-operational)' | PASSTHROUGH_OPERATIONAL.datastoreName | 'create'
253 def 'Get batch resource data when notification feature is disabled for datastore: #datastore.'() {
254 given: 'batch resource data url'
255 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
256 def resourceDataBatchRequestJsonData = jsonObjectMapper.asJsonString(
257 getResourceDataBatchRequest("read", datastore.datastoreName))
258 ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = false
259 when: 'post data resource request is performed'
260 def response = mvc.perform(
262 .contentType(MediaType.APPLICATION_JSON)
263 .content(resourceDataBatchRequestJsonData)
264 ).andReturn().response
265 then: 'response status is Ok'
266 response.status == HttpStatus.OK.value()
267 and: 'async request id is unavailable'
268 assert response.contentAsString == '{"status":"Asynchronous request is unavailable as notification feature is currently disabled."}'
269 where: 'the following data stores are used'
270 datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
273 def 'Query Resource Data from operational.'() {
274 given: 'the query resource data url'
275 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query" +
276 "?cps-path=/cps/path"
277 when: 'the query data resource request is performed'
278 def response = mvc.perform(
280 .contentType(MediaType.APPLICATION_JSON)
281 ).andReturn().response
282 then: 'the NCMP query service is called with queryResourceDataOperationalForCmHandle'
283 1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle',
285 FetchDescendantsOption.OMIT_DESCENDANTS)
286 and: 'response status is Ok'
287 response.status == HttpStatus.OK.value()
290 def 'Query Resource Data using datastore of #datastore'() {
291 given: 'the query resource data url'
292 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastore}/query" +
293 "?cps-path=/cps/path"
294 when: 'the query data resource request is performed'
295 def response = mvc.perform(
297 .contentType(MediaType.APPLICATION_JSON)
298 ).andReturn().response
299 then: 'a 400 BAD_REQUEST is returned for the unsupported datastore'
300 response.status == 400
301 and: 'the error message is that datastore #datastore is not supported'
302 response.contentAsString.contains("ncmp-datastore:${datastore} is not supported")
303 where: 'the following datastore is used'
304 datastore << ["passthrough-running", "passthrough-operational"]
307 def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
308 given: 'resource data url'
309 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
310 "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)"
311 and: 'ncmp service returns json object'
312 mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle',
313 resourceIdentifier,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID) >> '{valid-json}'
314 when: 'get data resource request is performed'
315 def response = mvc.perform(
317 .contentType(MediaType.APPLICATION_JSON)
318 ).andReturn().response
319 then: 'response status is Ok'
320 response.status == HttpStatus.OK.value()
321 and: 'response contains valid object body'
322 response.getContentAsString() == '{valid-json}'
323 where: 'tokens are used in the resource identifier parameter'
324 scenario | resourceIdentifier
325 '/' | 'id/with/slashes'
330 '? needs to be encoded as %3F' | 'idWith%3F'
333 def 'Update resource data from pass-through running.'() {
334 given: 'update resource data url'
335 def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
336 "?resourceIdentifier=parent/child"
337 when: 'update data resource request is performed'
338 def response = mvc.perform(
340 .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
341 ).andReturn().response
342 then: 'ncmp service method to update resource is called'
343 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
344 'parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8')
345 and: 'the response status is OK'
346 response.status == HttpStatus.OK.value()
349 def 'Create Resource Data from pass-through running with #scenario.'() {
350 given: 'resource data url'
351 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
352 "?resourceIdentifier=parent/child"
353 when: 'create resource request is performed'
354 def response = mvc.perform(
356 .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
357 ).andReturn().response
358 then: 'ncmp service method to create resource called'
359 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
360 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8')
361 and: 'resource is created'
362 response.status == HttpStatus.CREATED.value()
365 def 'Get module references for the given dataspace and cm handle.'() {
366 given: 'get module references url'
367 def getUrl = "$ncmpBasePathV1/ch/some-cmhandle/modules"
368 when: 'get module resource request is performed'
369 def response = mvc.perform(get(getUrl)).andReturn().response
370 then: 'ncmp service method to get yang resource module references is called'
371 mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle')
372 >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')]
373 and: 'response contains an array with the module name and revision'
374 response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]'
375 and: 'response returns an OK http code'
376 response.status == HttpStatus.OK.value()
379 def 'Retrieve cm handles.'() {
380 given: 'an endpoint and json data'
381 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
382 String jsonString = TestUtils.getResourceFileContent('cmhandle-search.json')
383 and: 'the service method is invoked with module names and returns two cm handles'
384 def cmHandle1 = new NcmpServiceCmHandle()
385 cmHandle1.cmHandleId = 'some-cmhandle-id1'
386 cmHandle1.publicProperties = [color: 'yellow']
387 def cmHandle2 = new NcmpServiceCmHandle()
388 cmHandle2.cmHandleId = 'some-cmhandle-id2'
389 cmHandle2.publicProperties = [color: 'green']
390 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandle1, cmHandle2]
391 when: 'the searches api is invoked'
392 def response = mvc.perform(post(searchesEndpoint)
393 .contentType(MediaType.APPLICATION_JSON)
394 .content(jsonString)).andReturn().response
395 then: 'response status returns OK'
396 response.status == HttpStatus.OK.value()
397 and: 'the expected response content is returned'
398 response.contentAsString == '[{"cmHandle":"some-cmhandle-id1","publicCmHandleProperties":[{"color":"yellow"}],"state":null},{"cmHandle":"some-cmhandle-id2","publicCmHandleProperties":[{"color":"green"}],"state":null}]'
401 def 'Get complete Cm Handle details by Cm Handle id.'() {
402 given: 'an endpoint and a cm handle'
403 def cmHandleDetailsEndpoint = "$ncmpBasePathV1/ch/some-cm-handle"
404 and: 'an existing ncmp service cm handle'
405 def cmHandleId = 'some-cm-handle'
406 def dmiProperties = [prop: 'some DMI property']
407 def publicProperties = ["public prop": 'some public property']
408 def compositeState = compositeStateTestObject()
409 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
410 and: 'the service method is invoked with the cm handle id'
411 1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('some-cm-handle') >> ncmpServiceCmHandle
412 when: 'the cm handle details api is invoked'
413 def response = mvc.perform(
414 get(cmHandleDetailsEndpoint)).andReturn().response
415 then: 'the correct response is returned'
416 response.status == HttpStatus.OK.value()
417 and: 'the response contains the public properties'
418 assertContainsPublicProperties(response)
419 and: 'the response contains the cm handle state'
420 assertContainsState(response)
421 and: 'the content does not contain dmi properties'
422 !response.contentAsString.contains("some DMI property")
425 def 'Get Cm Handle public properties by Cm Handle id.'() {
426 given: 'a cm handle properties endpoint'
427 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/properties"
428 and: 'some cm handle public properties'
429 def publicProperties = ['public prop': 'some public property']
430 and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
431 1 * mockNetworkCmProxyDataService
432 .getCmHandlePublicProperties('some-cm-handle') >> publicProperties
433 when: 'the cm handle properties api is invoked'
434 def response = mvc.perform(
435 get(cmHandlePropertiesEndpoint)).andReturn().response
436 then: 'the correct response is returned'
437 response.status == HttpStatus.OK.value()
438 and: 'the response contains the public properties'
439 assertContainsPublicProperties(response)
442 def 'Get Cm Handle composite state by Cm Handle id.'() {
443 given: 'a cm handle state endpoint'
444 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/state"
445 and: 'some cm handle composite state'
446 def compositeState = compositeStateTestObject()
447 and: 'the service method is invoked with the cm handle id returning the cm handle composite state'
448 1 * mockNetworkCmProxyDataService
449 .getCmHandleCompositeState('some-cm-handle') >> compositeState
450 when: 'the cm handle state api is invoked'
451 def response = mvc.perform(
452 get(cmHandlePropertiesEndpoint)).andReturn().response
453 then: 'the correct response is returned'
454 response.status == HttpStatus.OK.value()
455 and: 'the response contains the cm handle state'
456 assertContainsState(response)
459 def 'Call execute cm handle searches with unrecognized condition name.'() {
460 given: 'an endpoint and json data'
461 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
462 String jsonString = TestUtils.getResourceFileContent('invalid-cmhandle-search.json')
463 and: 'the service method is invoked with module names and returns two cm handles'
464 def cmHandel1 = new NcmpServiceCmHandle()
465 cmHandel1.cmHandleId = 'some-cmhandle-id1'
466 cmHandel1.publicProperties = [color: 'yellow']
467 def cmHandel2 = new NcmpServiceCmHandle()
468 cmHandel2.cmHandleId = 'some-cmhandle-id2'
469 cmHandel2.publicProperties = [color: 'green']
470 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandel1, cmHandel2]
471 when: 'the searches api is invoked'
472 def response = mvc.perform(
473 post(searchesEndpoint)
474 .contentType(MediaType.APPLICATION_JSON)
475 .content(jsonString)).andReturn().response
476 then: 'an empty cm handle identifier is returned'
477 response.contentAsString == '[{"cmHandle":"some-cmhandle-id1","publicCmHandleProperties":[{"color":"yellow"}],"state":null},{"cmHandle":"some-cmhandle-id2","publicCmHandleProperties":[{"color":"green"}],"state":null}]'
480 def 'Query for cm handles matching query parameters'() {
481 given: 'an endpoint and json data'
482 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
483 and: 'the service method is invoked with module names and returns cm handle ids'
484 1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2']
485 when: 'the searches api is invoked'
486 def response = mvc.perform(
487 post(searchesEndpoint)
488 .contentType(MediaType.APPLICATION_JSON)
489 .content('{}')).andReturn().response
490 then: 'cm handle ids are returned'
491 response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]'
494 def 'Query for cm handles with invalid request payload'() {
495 when: 'the searches api is invoked'
496 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
497 def invalidInputData = '{invalidJson}'
498 def response = mvc.perform(
499 post(searchesEndpoint)
500 .contentType(MediaType.APPLICATION_JSON)
501 .content(invalidInputData)).andReturn().response
502 then: 'BAD_REQUEST is returned'
503 response.getStatus() == 400
506 def 'Patch resource data in pass-through running datastore.'() {
507 given: 'patch resource data url'
508 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
509 "?resourceIdentifier=parent/child"
510 when: 'patch data resource request is performed'
511 def response = mvc.perform(
513 .contentType(MediaType.APPLICATION_JSON)
514 .accept(MediaType.APPLICATION_JSON).content(requestBody)
515 ).andReturn().response
516 then: 'ncmp service method to update resource is called'
517 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
518 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8')
519 and: 'the response status is OK'
520 response.status == HttpStatus.OK.value()
523 def 'Delete resource data in pass-through running datastore.'() {
524 given: 'delete resource data url'
525 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
526 "?resourceIdentifier=parent/child"
527 when: 'delete data resource request is performed'
528 def response = mvc.perform(
530 .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
531 then: 'the ncmp service method to delete resource is called (with null as body)'
532 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
533 'parent/child', DELETE, null, 'application/json;charset=UTF-8')
534 and: 'the response is No Content'
535 response.status == HttpStatus.NO_CONTENT.value()
538 def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
539 given: 'resource data url'
540 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
541 "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
542 when: 'get data resource request is performed'
543 def response = mvc.perform(
545 .contentType(MediaType.APPLICATION_JSON)
546 .accept(MediaType.APPLICATION_JSON_VALUE)
547 ).andReturn().response
548 then: 'async request id is generated'
549 assert response.contentAsString.contains("requestId")
550 where: 'the following parameters are used'
551 scenario | datastoreInUrl
552 ':passthrough-operational' | 'passthrough-operational'
553 ':passthrough-running' | 'passthrough-running'
556 def 'Get module definitions based on cmHandleId.'() {
557 when: 'get module definition request is performed'
558 def response = mvc.perform(
559 get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions"))
560 .andReturn().response
561 then: 'ncmp service method to get module definitions is called'
562 mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleId('some-cmhandle')
563 >> [new ModuleDefinition('sampleModuleName', '2021-10-03',
564 'module sampleModuleName{ sample module content }')]
565 and: 'response contains an array with the module name, revision and content'
566 response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]'
567 and: 'response returns an OK http code'
568 response.status == HttpStatus.OK.value()
571 def 'Set the data sync enabled based on the cm handle id and the data sync flag is #scenario'() {
572 when: 'the set data sync enabled request is invoked'
573 def response = mvc.perform(
574 put("$ncmpBasePathV1/ch/some-cm-handle-id/data-sync?dataSyncEnabled=" + dataSyncEnabledFlag))
575 .andReturn().response
576 then: 'method to set data sync enabled is called'
577 1 * mockNetworkCmProxyDataService.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
578 and: 'the response returns an OK http code'
579 response.status == HttpStatus.OK.value()
580 where: 'the following parameters are used'
581 scenario | dataSyncEnabledFlag
586 def 'Get Resource Data from operational with or without descendants'() {
587 given: 'resource data url with descendants #enabled'
588 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational" +
589 "?resourceIdentifier=parent/child&include-descendants=${enabled}"
590 when: 'get data resource request is performed'
591 def response = mvc.perform(
593 .contentType(MediaType.APPLICATION_JSON)
594 ).andReturn().response
595 then: 'the NCMP data service is called with getResourceDataOperational with #descendantsOption'
596 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child', descendantsOption)
597 and: 'response status is Ok'
598 response.status == HttpStatus.OK.value()
599 where: 'the following parameters are used'
600 enabled | descendantsOption
601 false | FetchDescendantsOption.OMIT_DESCENDANTS
602 true | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
605 def 'Attempt execute #operation rest operation on resource data with #scenario'() {
606 given: 'resource data url'
607 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/${datastoreInUrl}?resourceIdentifier=parent/child"
608 when: 'selected request for data resource is performed on url'
609 def response = mvc.perform(
610 executeRestOperation(operation, url))
611 .andReturn().response
612 then: 'the response status is as expected'
613 assert response.status == HttpStatus.BAD_REQUEST.value()
614 and: 'the response is as expected'
615 assert response.getContentAsString().contains(datastoreInUrl)
616 where: 'the following parameters are used'
617 scenario | operation | datastoreInUrl
618 'unsupported datastore' | 'POST' | 'ncmp-datastore:operational'
619 'invalid datastore' | 'POST' | 'invalid'
620 'unsupported datastore' | 'PUT' | 'ncmp-datastore:operational'
621 'invalid datastore' | 'PUT' | 'invalid'
622 'unsupported datastore' | 'PATCH' | 'ncmp-datastore:operational'
623 'invalid datastore' | 'PATCH' | 'invalid'
624 'unsupported datastore' | 'DELETE' | 'ncmp-datastore:operational'
625 'invalid datastore' | 'DELETE' | 'invalid'
628 def executeRestOperation(operation, url) {
629 if (operation == 'POST') {
630 return post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
632 if (operation == 'PUT') {
633 return put(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
635 if (operation == 'PATCH') {
636 return patch(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
638 if (operation == 'DELETE') {
639 return delete(url).contentType(MediaType.APPLICATION_JSON_VALUE)
645 .operationalDataStore(Operational.builder()
646 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
647 .lastSyncTime(formattedDateAndTime.toString()).build()).build()
650 def compositeStateTestObject() {
651 new CompositeState(cmHandleState: CmHandleState.ADVISED,
652 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
653 lastUpdateTime: formattedDateAndTime.toString(),
654 dataSyncEnabled: false,
655 dataStores: dataStores())
658 def assertContainsAll(response, assertContent) {
659 assertContent.forEach(string -> { assert (response.contentAsString.contains(string)) })
663 def assertContainsState(response) {
664 def expectedContent = [
666 '"cmHandleState":"ADVISED"',
667 '"reason":"LOCKED_MISBEHAVING"',
668 '"details":"lock details"',
669 '"lastUpdateTime":"2022-12-31T20:30:40.000+0000"',
670 '"dataSyncEnabled":false',
673 '"syncState":"NONE_REQUESTED"',
674 '"lastSyncTime":"2022-12-31T20:30:40.000+0000"',
677 return assertContainsAll(response, expectedContent)
680 def assertContainsPublicProperties(response) {
681 def expectedContent = [
682 '"publicCmHandleProperties":',
684 '"some public property"'
686 return assertContainsAll(response, expectedContent)
689 def getResourceDataBatchRequest(operation, datastore) {
690 def resourceDataBatchRequest = new ResourceDataBatchRequest()
691 def batchOperationDefinitions = new ArrayList()
692 batchOperationDefinitions.add(getBatchOperationDefinition(operation, datastore))
693 resourceDataBatchRequest.addOperationsItem(batchOperationDefinitions)
696 def getBatchOperationDefinition(operation, datastore) {
697 def batchOperationDefinition = new BatchOperationDefinition()
698 batchOperationDefinition.setOperation(operation)
699 batchOperationDefinition.setOperationId("operational-12")
700 batchOperationDefinition.setDatastore(datastore)
701 batchOperationDefinition.setOptions("some option")
702 batchOperationDefinition.setResourceIdentifier("some resource identifier")
703 batchOperationDefinition.addTargetIdsItem("some-cm-handle")
704 return batchOperationDefinition