2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021 Pantheon.tech
4 * Modifications Copyright (C) 2021 highstreet technologies GmbH
5 * Modifications Copyright (C) 2021-2024 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 ch.qos.logback.classic.Level
27 import ch.qos.logback.classic.Logger
28 import ch.qos.logback.classic.spi.ILoggingEvent
29 import ch.qos.logback.core.read.ListAppender
30 import com.fasterxml.jackson.databind.ObjectMapper
31 import org.mapstruct.factory.Mappers
32 import org.onap.cps.TestUtils
33 import org.onap.cps.events.EventsPublisher
34 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
35 import org.onap.cps.ncmp.api.NetworkCmProxyQueryService
36 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
37 import org.onap.cps.ncmp.api.impl.inventory.CompositeState
38 import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState
39 import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory
40 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel
41 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
42 import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler
43 import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler
44 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
45 import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper
46 import org.onap.cps.ncmp.rest.mapper.DataOperationRequestMapper
47 import org.onap.cps.ncmp.rest.model.DataOperationDefinition
48 import org.onap.cps.ncmp.rest.model.DataOperationRequest
49 import org.onap.cps.ncmp.rest.util.DeprecationHelper
50 import org.onap.cps.spi.FetchDescendantsOption
51 import org.onap.cps.spi.model.ModuleDefinition
52 import org.onap.cps.spi.model.ModuleReference
53 import org.onap.cps.utils.JsonObjectMapper
54 import org.slf4j.LoggerFactory
55 import org.spockframework.spring.SpringBean
56 import org.springframework.beans.factory.annotation.Autowired
57 import org.springframework.beans.factory.annotation.Value
58 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
59 import org.springframework.http.HttpStatus
60 import org.springframework.http.MediaType
61 import org.springframework.test.web.servlet.MockMvc
62 import spock.lang.Shared
63 import spock.lang.Specification
65 import java.time.OffsetDateTime
66 import java.time.ZoneOffset
67 import java.time.format.DateTimeFormatter
69 import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.DataStores
70 import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.Operational
71 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
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.OperationType.CREATE
75 import static org.onap.cps.ncmp.api.impl.operations.OperationType.DELETE
76 import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH
77 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
78 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
79 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
80 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
81 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
82 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
83 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
84 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
86 @WebMvcTest(NetworkCmProxyController)
87 class NetworkCmProxyControllerSpec extends Specification {
93 NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
96 NetworkCmProxyQueryService mockNetworkCmProxyQueryService = Mock()
99 ObjectMapper objectMapper = new ObjectMapper()
102 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
105 NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
108 CmHandleStateMapper cmHandleStateMapper = Mappers.getMapper(CmHandleStateMapper)
111 DataOperationRequestMapper dataOperationRequestMapper = Mappers.getMapper(DataOperationRequestMapper)
114 Map<String, TrustLevel> trustLevelPerCmHandle = [:]
117 CpsNcmpTaskExecutor mockCpsTaskExecutor = Mock()
120 DeprecationHelper stubbedDeprecationHelper = Stub()
123 NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler = new NcmpCachedResourceRequestHandler(mockCpsTaskExecutor, mockNetworkCmProxyDataService, mockNetworkCmProxyQueryService)
126 NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler = new NcmpPassthroughResourceRequestHandler(mockCpsTaskExecutor, mockNetworkCmProxyDataService)
128 @Value('${rest.api.ncmp-base-path}/v1')
131 def requestBody = '{"some-key":"some-value"}'
133 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
137 def NO_REQUEST_ID = null
138 def NO_AUTH_HEADER = null
139 def TIMOUT_FOR_TEST = 1234
141 def logger = Spy(ListAppender<ILoggingEvent>)
144 ncmpCachedResourceRequestHandler.notificationFeatureEnabled = true
145 ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
146 ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = true
147 ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
152 ((Logger) LoggerFactory.getLogger(EventsPublisher.class)).detachAndStopAllAppenders()
155 def 'Get Resource Data from pass-through operational.'() {
156 given: 'resource data url'
157 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
158 "?resourceIdentifier=parent/child&options=(a=1,b=2)"
159 when: 'get data resource request is performed'
160 def response = mvc.perform(
162 .contentType(MediaType.APPLICATION_JSON)
163 ).andReturn().response
164 then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle'
165 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle',
166 'parent/child','(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
167 and: 'response status is Ok'
168 response.status == HttpStatus.OK.value()
171 def 'Get Resource Data from ncmp-datastore:operational (cached) parameters handling with #scenario.'() {
172 given: 'resource data url'
173 def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational" +
174 "?resourceIdentifier=parent/child${additionalUrlParam}"
175 when: 'get data resource request is performed'
176 def response = mvc.perform(
177 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
178 then: 'task executor is called appropriate number of times'
179 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle('ncmp-datastore:operational', 'h123', 'parent/child', expectedIncludeDescendants)
180 and: 'response status is OK'
181 response.status == HttpStatus.OK.value()
182 where: 'the following parameters are used'
183 scenario | additionalUrlParam || expectedIncludeDescendants
184 'no additional param' | '' || OMIT_DESCENDANTS
185 'include descendants true' | '&include-descendants=true' || INCLUDE_ALL_DESCENDANTS
186 'include descendants TRUE' | '&include-descendants=true' || INCLUDE_ALL_DESCENDANTS
187 'include descendants false' | '&include-descendants=false' || OMIT_DESCENDANTS
188 'include descendants FALSE' | '&include-descendants=FALSE' || OMIT_DESCENDANTS
189 'options (ignored)' | '&options=(a-=1)' || OMIT_DESCENDANTS
192 def 'Execute (async) data operation to read data from dmi service.'() {
193 given: 'data operation url'
194 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
195 def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest("read", datastore.datastoreName))
196 when: 'post data operation request is performed'
197 def response = mvc.perform(
199 .contentType(MediaType.APPLICATION_JSON)
200 .content(dataOperationRequestJsonData)
201 ).andReturn().response
202 then: 'response status is Ok'
203 response.status == HttpStatus.OK.value()
204 and: 'async request id is generated'
205 assert response.contentAsString.contains('requestId')
206 then: 'the request is handled asynchronously'
207 1 * mockCpsTaskExecutor.executeTask(*_)
208 where: 'the following data stores are used'
209 datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
212 def 'Execute (async) data operation with some validation error.'() {
213 given: 'data operation url'
214 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
215 def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(
216 getDataOperationRequest('read', 'invalid datastore'))
217 when: 'post data resource request is performed'
218 def response = mvc.perform(
220 .contentType(MediaType.APPLICATION_JSON)
221 .content(dataOperationRequestJsonData)
222 ).andReturn().response
223 then: 'response status is BAD_REQUEST'
224 response.status == HttpStatus.BAD_REQUEST.value()
227 def 'Get data operation resource data when notification feature is disabled for datastore: #datastore.'() {
228 given: 'data operation url'
229 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
230 def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(
231 getDataOperationRequest("read", PASSTHROUGH_RUNNING.datastoreName))
232 ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = false
233 when: 'post data resource request is performed'
234 def response = mvc.perform(
236 .contentType(MediaType.APPLICATION_JSON)
237 .content(dataOperationRequestJsonData)
238 ).andReturn().response
239 then: 'response status is Ok'
240 response.status == HttpStatus.OK.value()
241 and: 'async request id is unavailable'
242 assert response.contentAsString == '{"status":"Asynchronous request is unavailable as notification feature is currently disabled."}'
245 def 'Query Resource Data from operational.'() {
246 given: 'the query resource data url'
247 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query" +
248 "?cps-path=/cps/path"
249 when: 'the query data resource request is performed'
250 def response = mvc.perform(
252 .contentType(MediaType.APPLICATION_JSON)
253 ).andReturn().response
254 then: 'the NCMP query service is called with queryResourceDataOperationalForCmHandle'
255 1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle',
257 FetchDescendantsOption.OMIT_DESCENDANTS)
258 and: 'response status is Ok'
259 response.status == HttpStatus.OK.value()
262 def 'Query Resource Data with unsupported datastore'() {
263 given: 'the query resource data url'
264 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running/query" +
265 "?cps-path=/cps/path"
266 when: 'the query data resource request is performed'
267 def response = mvc.perform(
269 .contentType(MediaType.APPLICATION_JSON)
270 ).andReturn().response
271 then: 'a 400 BAD_REQUEST is returned for the unsupported datastore'
272 response.status == 400
273 and: 'the error message is that the datastore is not supported'
274 response.contentAsString.contains("ncmp-datastore:passthrough-running is not supported")
277 def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
278 given: 'resource data url'
279 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
280 "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)"
281 and: 'ncmp service returns json object'
282 mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle',
283 resourceIdentifier,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> '{valid-json}'
284 when: 'get data resource request is performed'
285 def response = mvc.perform(
287 .contentType(MediaType.APPLICATION_JSON)
288 ).andReturn().response
289 then: 'response status is Ok'
290 response.status == HttpStatus.OK.value()
291 and: 'response contains valid object body'
292 response.getContentAsString() == '{valid-json}'
293 where: 'tokens are used in the resource identifier parameter'
294 scenario | resourceIdentifier
295 '/' | 'id/with/slashes'
300 '? needs to be encoded as %3F' | 'idWith%3F'
303 def 'Update resource data from pass-through running.'() {
304 given: 'update resource data url'
305 def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
306 "?resourceIdentifier=parent/child"
307 when: 'update data resource request is performed'
308 def response = mvc.perform(
310 .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
311 ).andReturn().response
312 then: 'ncmp service method to update resource is called'
313 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
314 'parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
315 and: 'the response status is OK'
316 response.status == HttpStatus.OK.value()
319 def 'Create Resource Data from pass-through running with #scenario.'() {
320 given: 'resource data url'
321 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
322 "?resourceIdentifier=parent/child"
323 when: 'create resource request is performed'
324 def response = mvc.perform(
326 .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
327 ).andReturn().response
328 then: 'ncmp service method to create resource called'
329 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
330 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
331 and: 'resource is created'
332 response.status == HttpStatus.CREATED.value()
335 def 'Get module references for the given dataspace and cm handle.'() {
336 given: 'get module references url'
337 def getUrl = "$ncmpBasePathV1/ch/some-cmhandle/modules"
338 when: 'get module resource request is performed'
339 def response = mvc.perform(get(getUrl)).andReturn().response
340 then: 'ncmp service method to get yang resource module references is called'
341 mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle')
342 >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')]
343 and: 'response contains an array with the module name and revision'
344 response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]'
345 and: 'response returns an OK http code'
346 response.status == HttpStatus.OK.value()
349 def 'Retrieve cm handles.'() {
350 given: 'an endpoint and json data'
351 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
352 String jsonString = TestUtils.getResourceFileContent('cmhandle-search.json')
353 and: 'the service method is invoked with module names and returns two cm handles'
354 def cmHandle1 = new NcmpServiceCmHandle()
355 cmHandle1.cmHandleId = 'ch-1'
356 cmHandle1.publicProperties = [color: 'yellow']
357 def cmHandle2 = new NcmpServiceCmHandle()
358 cmHandle2.cmHandleId = 'ch-2'
359 cmHandle2.publicProperties = [color: 'green']
360 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandle1, cmHandle2]
361 and: 'map for trust level per cmHandle has value for only one cm handle'
362 // trustLevelPerCmHandle.get('') >> { TrustLevel.NONE }
363 trustLevelPerCmHandle.put('ch-1', TrustLevel.NONE)
364 when: 'the searches api is invoked'
365 def response = mvc.perform(post(searchesEndpoint)
366 .contentType(MediaType.APPLICATION_JSON)
367 .content(jsonString)).andReturn().response
368 then: 'response status returns OK'
369 response.status == HttpStatus.OK.value()
370 and: 'the expected response content is returned'
371 response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"NONE"},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":null}]'
374 def 'Get complete Cm Handle details by Cm Handle id.'() {
375 given: 'an endpoint and a cm handle'
376 def cmHandleDetailsEndpoint = "$ncmpBasePathV1/ch/some-cm-handle"
377 and: 'an existing ncmp service cm handle'
378 def cmHandleId = 'some-cm-handle'
379 def dmiProperties = [prop: 'some DMI property']
380 def publicProperties = ["public prop": 'some public property']
381 def compositeState = compositeStateTestObject()
382 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
383 and: 'the service method is invoked with the cm handle id'
384 1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('some-cm-handle') >> ncmpServiceCmHandle
385 and: 'map for trust level per cmHandle has values'
386 trustLevelPerCmHandle.get('some-cm-handle') >> { TrustLevel.COMPLETE }
387 when: 'the cm handle details api is invoked'
388 def response = mvc.perform(
389 get(cmHandleDetailsEndpoint)).andReturn().response
390 then: 'the correct response is returned'
391 response.status == HttpStatus.OK.value()
392 and: 'the response contains the public properties'
393 assertContainsPublicProperties(response)
394 and: 'the response contains the cm handle state'
395 assertContainsState(response)
396 and: 'the content does not contain dmi properties'
397 !response.contentAsString.contains("some DMI property")
400 def 'Get Cm Handle public properties by Cm Handle id.'() {
401 given: 'a cm handle properties endpoint'
402 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/properties"
403 and: 'some cm handle public properties'
404 def publicProperties = ['public prop': 'some public property']
405 and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
406 1 * mockNetworkCmProxyDataService
407 .getCmHandlePublicProperties('some-cm-handle') >> publicProperties
408 when: 'the cm handle properties api is invoked'
409 def response = mvc.perform(
410 get(cmHandlePropertiesEndpoint)).andReturn().response
411 then: 'the correct response is returned'
412 response.status == HttpStatus.OK.value()
413 and: 'the response contains the public properties'
414 assertContainsPublicProperties(response)
417 def 'Get Cm Handle composite state by Cm Handle id.'() {
418 given: 'a cm handle state endpoint'
419 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/state"
420 and: 'some cm handle composite state'
421 def compositeState = compositeStateTestObject()
422 and: 'the service method is invoked with the cm handle id returning the cm handle composite state'
423 1 * mockNetworkCmProxyDataService
424 .getCmHandleCompositeState('some-cm-handle') >> compositeState
425 when: 'the cm handle state api is invoked'
426 def response = mvc.perform(
427 get(cmHandlePropertiesEndpoint)).andReturn().response
428 then: 'the correct response is returned'
429 response.status == HttpStatus.OK.value()
430 and: 'the response contains the cm handle state'
431 assertContainsState(response)
434 def 'Call execute cm handle searches with unrecognized condition name.'() {
435 given: 'an endpoint and json data'
436 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
437 String jsonString = TestUtils.getResourceFileContent('invalid-cmhandle-search.json')
438 and: 'the service method is invoked with module names and returns two cm handles'
439 def cmHandel1 = new NcmpServiceCmHandle()
440 cmHandel1.cmHandleId = 'ch-1'
441 cmHandel1.publicProperties = [color: 'yellow']
442 def cmHandel2 = new NcmpServiceCmHandle()
443 cmHandel2.cmHandleId = 'ch-2'
444 cmHandel2.publicProperties = [color: 'green']
445 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandel1, cmHandel2]
446 and: 'map for trust level per cmHandle has values'
447 trustLevelPerCmHandle.put('ch-1', TrustLevel.COMPLETE)
448 trustLevelPerCmHandle.put('ch-2', TrustLevel.NONE)
449 when: 'the searches api is invoked'
450 def response = mvc.perform(
451 post(searchesEndpoint)
452 .contentType(MediaType.APPLICATION_JSON)
453 .content(jsonString)).andReturn().response
454 then: 'an empty cm handle identifier is returned'
455 response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"COMPLETE"},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":"NONE"}]'
458 def 'Query for cm handles matching query parameters'() {
459 given: 'an endpoint and json data'
460 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
461 and: 'the service method is invoked with module names and returns cm handle ids'
462 1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['ch-1', 'ch-2']
463 when: 'the searches api is invoked'
464 def response = mvc.perform(
465 post(searchesEndpoint)
466 .contentType(MediaType.APPLICATION_JSON)
467 .content('{}')).andReturn().response
468 then: 'cm handle ids are returned'
469 response.contentAsString == '["ch-1","ch-2"]'
472 def 'Query for cm handles with invalid request payload'() {
473 when: 'the searches api is invoked'
474 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
475 def invalidInputData = '{invalidJson}'
476 def response = mvc.perform(
477 post(searchesEndpoint)
478 .contentType(MediaType.APPLICATION_JSON)
479 .content(invalidInputData)).andReturn().response
480 then: 'BAD_REQUEST is returned'
481 response.getStatus() == 400
484 def 'Patch resource data in pass-through running datastore.'() {
485 given: 'patch resource data url'
486 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
487 "?resourceIdentifier=parent/child"
488 when: 'patch data resource request is performed'
489 def response = mvc.perform(
491 .contentType(MediaType.APPLICATION_JSON)
492 .accept(MediaType.APPLICATION_JSON).content(requestBody)
493 ).andReturn().response
494 then: 'ncmp service method to update resource is called'
495 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
496 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
497 and: 'the response status is OK'
498 response.status == HttpStatus.OK.value()
501 def 'Delete resource data in pass-through running datastore.'() {
502 given: 'delete resource data url'
503 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
504 "?resourceIdentifier=parent/child"
505 when: 'delete data resource request is performed'
506 def response = mvc.perform(
508 .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
509 then: 'the ncmp service method to delete resource is called (with null as body)'
510 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
511 'parent/child', DELETE, null, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
512 and: 'the response is No Content'
513 response.status == HttpStatus.NO_CONTENT.value()
516 def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
517 given: 'resource data url'
518 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
519 "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
520 when: 'get data resource request is performed'
521 def response = mvc.perform(
523 .contentType(MediaType.APPLICATION_JSON)
524 .accept(MediaType.APPLICATION_JSON_VALUE)
525 ).andReturn().response
526 then: 'async request id is generated'
527 assert response.contentAsString.contains("requestId")
528 where: 'the following parameters are used'
529 scenario | datastoreInUrl
530 ':passthrough-operational' | 'passthrough-operational'
531 ':passthrough-running' | 'passthrough-running'
534 def 'Getting module definitions for a module'() {
535 when: 'get module definition request is performed with module name'
536 def response = mvc.perform(
537 get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=sampleModuleName"))
538 .andReturn().response
539 then: 'ncmp service method is invoked with correct parameters'
540 mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', 'sampleModuleName', _)
541 >> [new ModuleDefinition('sampleModuleName', '2021-10-03',
542 'module sampleModuleName{ sample module content }')]
543 and: 'response contains an array with the module name, revision and content'
544 response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]'
545 and: 'response returns an OK http code'
546 response.status == HttpStatus.OK.value()
549 def 'Getting module definitions filtering on #scenario'() {
550 when: 'get module definition request is performed'
551 def response = mvc.perform(
552 get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=" + moduleName + "&revision=" + revision))
553 .andReturn().response
554 then: 'ncmp service method to get definitions by cm handle is invoked when needed'
555 numberOfCallsToByCmHandleId * mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleId('some-cmhandle') >> []
556 and: 'ncmp service method to get definitions by module is invoked when needed'
557 numberOfCallsToByModule * mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', moduleName, revision) >> []
558 and: 'response returns an OK http code'
559 response.status == HttpStatus.OK.value()
560 and: 'the correct message is logged when needed'
561 if (expectLogWarning) {
562 def lastLoggingEvent = logger.list[0]
563 assert lastLoggingEvent.level == Level.WARN
564 assert lastLoggingEvent.formattedMessage.contains('Ignoring revision')
566 where: 'following parameters are used'
567 scenario | moduleName | revision || numberOfCallsToByCmHandleId | numberOfCallsToByModule | expectLogWarning
568 'module name' | 'some-module' | '' || 0 | 1 | false
569 'module name and revision' | 'some-module' | 'some-revision' || 0 | 1 | false
570 'no filtering' | '' | '' || 1 | 0 | false
571 'only revision' | '' | 'some-revision' || 1 | 0 | true
574 def 'Set the data sync enabled based on the cm handle id and the data sync flag is #scenario'() {
575 when: 'the set data sync enabled request is invoked'
576 def response = mvc.perform(
577 put("$ncmpBasePathV1/ch/some-cm-handle-id/data-sync?dataSyncEnabled=" + dataSyncEnabledFlag))
578 .andReturn().response
579 then: 'method to set data sync enabled is called'
580 1 * mockNetworkCmProxyDataService.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
581 and: 'the response returns an OK http code'
582 response.status == HttpStatus.OK.value()
583 where: 'the following parameters are used'
584 scenario | dataSyncEnabledFlag
589 def 'Get Resource Data from operational with or without descendants'() {
590 given: 'resource data url with descendants #enabled'
591 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational" +
592 "?resourceIdentifier=parent/child&include-descendants=${booleanValue}"
593 when: 'get data resource request is performed'
594 def response = mvc.perform(
596 .contentType(MediaType.APPLICATION_JSON)
597 ).andReturn().response
598 then: 'the NCMP data service is called with getResourceDataOperational with #descendantsOption'
599 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child', descendantsOption)
600 and: 'response status is Ok'
601 response.status == HttpStatus.OK.value()
602 where: 'the following parameters are used'
603 booleanValue | descendantsOption
604 false | OMIT_DESCENDANTS
605 true | INCLUDE_ALL_DESCENDANTS
608 def 'Attempt execute #operation rest operation on resource data with #scenario'() {
609 given: 'resource data url'
610 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/${datastoreInUrl}?resourceIdentifier=parent/child"
611 when: 'selected request for data resource is performed on url'
612 def response = mvc.perform(
613 executeRestOperation(operation, url))
614 .andReturn().response
615 then: 'the response status is as expected'
616 assert response.status == HttpStatus.BAD_REQUEST.value()
617 and: 'the response is as expected'
618 assert response.getContentAsString().contains(datastoreInUrl)
619 where: 'the following parameters are used'
620 scenario | operation | datastoreInUrl
621 'unsupported datastore' | 'POST' | 'ncmp-datastore:operational'
622 'invalid datastore' | 'POST' | 'invalid'
623 'unsupported datastore' | 'PUT' | 'ncmp-datastore:operational'
624 'invalid datastore' | 'PUT' | 'invalid'
625 'unsupported datastore' | 'PATCH' | 'ncmp-datastore:operational'
626 'invalid datastore' | 'PATCH' | 'invalid'
627 'unsupported datastore' | 'DELETE' | 'ncmp-datastore:operational'
628 'invalid datastore' | 'DELETE' | 'invalid'
631 def executeRestOperation(operation, url) {
632 if (operation == 'POST') {
633 return post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
635 if (operation == 'PUT') {
636 return put(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
638 if (operation == 'PATCH') {
639 return patch(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
641 if (operation == 'DELETE') {
642 return delete(url).contentType(MediaType.APPLICATION_JSON_VALUE)
648 .operationalDataStore(Operational.builder()
649 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
650 .lastSyncTime(formattedDateAndTime.toString()).build()).build()
653 def compositeStateTestObject() {
654 new CompositeState(cmHandleState: CmHandleState.ADVISED,
655 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
656 lastUpdateTime: formattedDateAndTime.toString(),
657 dataSyncEnabled: false,
658 dataStores: dataStores())
661 def assertContainsAll(response, assertContent) {
662 assertContent.forEach(string -> { assert (response.contentAsString.contains(string)) })
666 def assertContainsState(response) {
667 def expectedContent = [
669 '"cmHandleState":"ADVISED"',
670 '"lockReason":{"reason":"MODULE_SYNC_FAILED","details":"lock details"}',
671 '"lastUpdateTime":"2022-12-31T20:30:40.000+0000"',
672 '"dataSyncEnabled":false',
675 '"syncState":"NONE_REQUESTED"',
676 '"lastSyncTime":"2022-12-31T20:30:40.000+0000"',
679 return assertContainsAll(response, expectedContent)
682 def assertContainsPublicProperties(response) {
683 def expectedContent = [
684 '"publicCmHandleProperties":',
686 '"some public property"'
688 return assertContainsAll(response, expectedContent)
691 def getDataOperationRequest(operation, datastore) {
692 def dataOperationRequest = new DataOperationRequest()
693 def dataOperationDefinitions = new ArrayList()
694 dataOperationDefinitions.add(getDataOperationDefinition(operation, datastore))
695 dataOperationRequest.addOperationsItem(dataOperationDefinitions)
696 return dataOperationRequest
699 def getDataOperationDefinition(operation, datastore) {
700 def dataOperationDefinition = new DataOperationDefinition()
701 dataOperationDefinition.setOperation(operation)
702 dataOperationDefinition.setOperationId("operational-12")
703 dataOperationDefinition.setDatastore(datastore)
704 dataOperationDefinition.setOptions("some option")
705 dataOperationDefinition.setResourceIdentifier("some resource identifier")
706 dataOperationDefinition.addTargetIdsItem("some-cm-handle")
707 return dataOperationDefinition
711 def setupLogger = ((Logger) LoggerFactory.getLogger(NetworkCmProxyController.class))
712 setupLogger.setLevel(Level.DEBUG)
713 setupLogger.addAppender(logger)