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 cmHandle2.alternateId = 'someAlternateId'
361 cmHandle2.moduleSetTag = 'someModuleSetTag'
362 cmHandle2.dataProducerIdentifier = 'someDataProducerIdentifier'
363 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandle1, cmHandle2]
364 and: 'map for trust level per cmHandle has value for only one cm handle'
365 trustLevelPerCmHandle.put('ch-1', TrustLevel.NONE)
366 when: 'the searches api is invoked'
367 def response = mvc.perform(post(searchesEndpoint)
368 .contentType(MediaType.APPLICATION_JSON)
369 .content(jsonString)).andReturn().response
370 then: 'response status returns OK'
371 response.status == HttpStatus.OK.value()
372 and: 'the expected response content is returned'
373 response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":null,"moduleSetTag":"someModuleSetTag","alternateId":"someAlternateId","dataProducerIdentifier":"someDataProducerIdentifier"}]'
376 def 'Get complete Cm Handle details by Cm Handle id.'() {
377 given: 'an endpoint and a cm handle'
378 def cmHandleDetailsEndpoint = "$ncmpBasePathV1/ch/some-cm-handle"
379 and: 'an existing ncmp service cm handle'
380 def cmHandleId = 'some-cm-handle'
381 def dmiProperties = [prop: 'some DMI property']
382 def publicProperties = ["public prop": 'some public property']
383 def compositeState = compositeStateTestObject()
384 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
385 and: 'the service method is invoked with the cm handle id'
386 1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('some-cm-handle') >> ncmpServiceCmHandle
387 and: 'map for trust level per cmHandle has values'
388 trustLevelPerCmHandle.get('some-cm-handle') >> { TrustLevel.COMPLETE }
389 when: 'the cm handle details api is invoked'
390 def response = mvc.perform(
391 get(cmHandleDetailsEndpoint)).andReturn().response
392 then: 'the correct response is returned'
393 response.status == HttpStatus.OK.value()
394 and: 'the response contains the public properties'
395 assertContainsPublicProperties(response)
396 and: 'the response contains the cm handle state'
397 assertContainsState(response)
398 and: 'the content does not contain dmi properties'
399 !response.contentAsString.contains("some DMI property")
402 def 'Get Cm Handle public properties by Cm Handle id.'() {
403 given: 'a cm handle properties endpoint'
404 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/properties"
405 and: 'some cm handle public properties'
406 def publicProperties = ['public prop': 'some public property']
407 and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
408 1 * mockNetworkCmProxyDataService
409 .getCmHandlePublicProperties('some-cm-handle') >> publicProperties
410 when: 'the cm handle properties api is invoked'
411 def response = mvc.perform(
412 get(cmHandlePropertiesEndpoint)).andReturn().response
413 then: 'the correct response is returned'
414 response.status == HttpStatus.OK.value()
415 and: 'the response contains the public properties'
416 assertContainsPublicProperties(response)
419 def 'Get Cm Handle composite state by Cm Handle id.'() {
420 given: 'a cm handle state endpoint'
421 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/state"
422 and: 'some cm handle composite state'
423 def compositeState = compositeStateTestObject()
424 and: 'the service method is invoked with the cm handle id returning the cm handle composite state'
425 1 * mockNetworkCmProxyDataService
426 .getCmHandleCompositeState('some-cm-handle') >> compositeState
427 when: 'the cm handle state api is invoked'
428 def response = mvc.perform(
429 get(cmHandlePropertiesEndpoint)).andReturn().response
430 then: 'the correct response is returned'
431 response.status == HttpStatus.OK.value()
432 and: 'the response contains the cm handle state'
433 assertContainsState(response)
436 def 'Call execute cm handle searches with unrecognized condition name.'() {
437 given: 'an endpoint and json data'
438 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
439 String jsonString = TestUtils.getResourceFileContent('invalid-cmhandle-search.json')
440 and: 'the service method is invoked with module names and returns two cm handles'
441 def cmHandel1 = new NcmpServiceCmHandle()
442 cmHandel1.cmHandleId = 'ch-1'
443 cmHandel1.publicProperties = [color: 'yellow']
444 def cmHandel2 = new NcmpServiceCmHandle()
445 cmHandel2.cmHandleId = 'ch-2'
446 cmHandel2.publicProperties = [color: 'green']
447 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandel1, cmHandel2]
448 and: 'map for trust level per cmHandle has values'
449 trustLevelPerCmHandle.put('ch-1', TrustLevel.COMPLETE)
450 trustLevelPerCmHandle.put('ch-2', TrustLevel.NONE)
451 when: 'the searches api is invoked'
452 def response = mvc.perform(
453 post(searchesEndpoint)
454 .contentType(MediaType.APPLICATION_JSON)
455 .content(jsonString)).andReturn().response
456 then: 'an empty cm handle identifier is returned'
457 response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"COMPLETE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null}]'
460 def 'Query for cm handles matching query parameters'() {
461 given: 'an endpoint and json data'
462 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
463 and: 'the service method is invoked with module names and returns cm handle ids'
464 1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['ch-1', 'ch-2']
465 when: 'the searches api is invoked'
466 def response = mvc.perform(
467 post(searchesEndpoint)
468 .contentType(MediaType.APPLICATION_JSON)
469 .content('{}')).andReturn().response
470 then: 'cm handle ids are returned'
471 response.contentAsString == '["ch-1","ch-2"]'
474 def 'Query for cm handles with invalid request payload'() {
475 when: 'the searches api is invoked'
476 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
477 def invalidInputData = '{invalidJson}'
478 def response = mvc.perform(
479 post(searchesEndpoint)
480 .contentType(MediaType.APPLICATION_JSON)
481 .content(invalidInputData)).andReturn().response
482 then: 'BAD_REQUEST is returned'
483 response.getStatus() == 400
486 def 'Patch resource data in pass-through running datastore.'() {
487 given: 'patch resource data url'
488 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
489 "?resourceIdentifier=parent/child"
490 when: 'patch data resource request is performed'
491 def response = mvc.perform(
493 .contentType(MediaType.APPLICATION_JSON)
494 .accept(MediaType.APPLICATION_JSON).content(requestBody)
495 ).andReturn().response
496 then: 'ncmp service method to update resource is called'
497 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
498 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
499 and: 'the response status is OK'
500 response.status == HttpStatus.OK.value()
503 def 'Delete resource data in pass-through running datastore.'() {
504 given: 'delete resource data url'
505 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
506 "?resourceIdentifier=parent/child"
507 when: 'delete data resource request is performed'
508 def response = mvc.perform(
510 .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
511 then: 'the ncmp service method to delete resource is called (with null as body)'
512 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
513 'parent/child', DELETE, null, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
514 and: 'the response is No Content'
515 response.status == HttpStatus.NO_CONTENT.value()
518 def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
519 given: 'resource data url'
520 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
521 "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
522 when: 'get data resource request is performed'
523 def response = mvc.perform(
525 .contentType(MediaType.APPLICATION_JSON)
526 .accept(MediaType.APPLICATION_JSON_VALUE)
527 ).andReturn().response
528 then: 'async request id is generated'
529 assert response.contentAsString.contains("requestId")
530 where: 'the following parameters are used'
531 scenario | datastoreInUrl
532 ':passthrough-operational' | 'passthrough-operational'
533 ':passthrough-running' | 'passthrough-running'
536 def 'Getting module definitions for a module'() {
537 when: 'get module definition request is performed with module name'
538 def response = mvc.perform(
539 get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=sampleModuleName"))
540 .andReturn().response
541 then: 'ncmp service method is invoked with correct parameters'
542 mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', 'sampleModuleName', _)
543 >> [new ModuleDefinition('sampleModuleName', '2021-10-03',
544 'module sampleModuleName{ sample module content }')]
545 and: 'response contains an array with the module name, revision and content'
546 response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]'
547 and: 'response returns an OK http code'
548 response.status == HttpStatus.OK.value()
551 def 'Getting module definitions filtering on #scenario'() {
552 when: 'get module definition request is performed'
553 def response = mvc.perform(
554 get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=" + moduleName + "&revision=" + revision))
555 .andReturn().response
556 then: 'ncmp service method to get definitions by cm handle is invoked when needed'
557 numberOfCallsToByCmHandleId * mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleId('some-cmhandle') >> []
558 and: 'ncmp service method to get definitions by module is invoked when needed'
559 numberOfCallsToByModule * mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', moduleName, revision) >> []
560 and: 'response returns an OK http code'
561 response.status == HttpStatus.OK.value()
562 and: 'the correct message is logged when needed'
563 if (expectLogWarning) {
564 def lastLoggingEvent = logger.list[0]
565 assert lastLoggingEvent.level == Level.WARN
566 assert lastLoggingEvent.formattedMessage.contains('Ignoring revision')
568 where: 'following parameters are used'
569 scenario | moduleName | revision || numberOfCallsToByCmHandleId | numberOfCallsToByModule | expectLogWarning
570 'module name' | 'some-module' | '' || 0 | 1 | false
571 'module name and revision' | 'some-module' | 'some-revision' || 0 | 1 | false
572 'no filtering' | '' | '' || 1 | 0 | false
573 'only revision' | '' | 'some-revision' || 1 | 0 | true
576 def 'Set the data sync enabled based on the cm handle id and the data sync flag is #scenario'() {
577 when: 'the set data sync enabled request is invoked'
578 def response = mvc.perform(
579 put("$ncmpBasePathV1/ch/some-cm-handle-id/data-sync?dataSyncEnabled=" + dataSyncEnabledFlag))
580 .andReturn().response
581 then: 'method to set data sync enabled is called'
582 1 * mockNetworkCmProxyDataService.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
583 and: 'the response returns an OK http code'
584 response.status == HttpStatus.OK.value()
585 where: 'the following parameters are used'
586 scenario | dataSyncEnabledFlag
591 def 'Get Resource Data from operational with or without descendants'() {
592 given: 'resource data url with descendants #enabled'
593 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational" +
594 "?resourceIdentifier=parent/child&include-descendants=${booleanValue}"
595 when: 'get data resource request is performed'
596 def response = mvc.perform(
598 .contentType(MediaType.APPLICATION_JSON)
599 ).andReturn().response
600 then: 'the NCMP data service is called with getResourceDataOperational with #descendantsOption'
601 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child', descendantsOption)
602 and: 'response status is Ok'
603 response.status == HttpStatus.OK.value()
604 where: 'the following parameters are used'
605 booleanValue | descendantsOption
606 false | OMIT_DESCENDANTS
607 true | INCLUDE_ALL_DESCENDANTS
610 def 'Attempt execute #operation rest operation on resource data with #scenario'() {
611 given: 'resource data url'
612 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/${datastoreInUrl}?resourceIdentifier=parent/child"
613 when: 'selected request for data resource is performed on url'
614 def response = mvc.perform(
615 executeRestOperation(operation, url))
616 .andReturn().response
617 then: 'the response status is as expected'
618 assert response.status == HttpStatus.BAD_REQUEST.value()
619 and: 'the response is as expected'
620 assert response.getContentAsString().contains(datastoreInUrl)
621 where: 'the following parameters are used'
622 scenario | operation | datastoreInUrl
623 'unsupported datastore' | 'POST' | 'ncmp-datastore:operational'
624 'invalid datastore' | 'POST' | 'invalid'
625 'unsupported datastore' | 'PUT' | 'ncmp-datastore:operational'
626 'invalid datastore' | 'PUT' | 'invalid'
627 'unsupported datastore' | 'PATCH' | 'ncmp-datastore:operational'
628 'invalid datastore' | 'PATCH' | 'invalid'
629 'unsupported datastore' | 'DELETE' | 'ncmp-datastore:operational'
630 'invalid datastore' | 'DELETE' | 'invalid'
633 def executeRestOperation(operation, url) {
634 if (operation == 'POST') {
635 return post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
637 if (operation == 'PUT') {
638 return put(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
640 if (operation == 'PATCH') {
641 return patch(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
643 if (operation == 'DELETE') {
644 return delete(url).contentType(MediaType.APPLICATION_JSON_VALUE)
650 .operationalDataStore(Operational.builder()
651 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
652 .lastSyncTime(formattedDateAndTime.toString()).build()).build()
655 def compositeStateTestObject() {
656 new CompositeState(cmHandleState: CmHandleState.ADVISED,
657 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
658 lastUpdateTime: formattedDateAndTime.toString(),
659 dataSyncEnabled: false,
660 dataStores: dataStores())
663 def assertContainsAll(response, assertContent) {
664 assertContent.forEach(string -> { assert (response.contentAsString.contains(string)) })
668 def assertContainsState(response) {
669 def expectedContent = [
671 '"cmHandleState":"ADVISED"',
672 '"lockReason":{"reason":"MODULE_SYNC_FAILED","details":"lock details"}',
673 '"lastUpdateTime":"2022-12-31T20:30:40.000+0000"',
674 '"dataSyncEnabled":false',
677 '"syncState":"NONE_REQUESTED"',
678 '"lastSyncTime":"2022-12-31T20:30:40.000+0000"',
681 return assertContainsAll(response, expectedContent)
684 def assertContainsPublicProperties(response) {
685 def expectedContent = [
686 '"publicCmHandleProperties":',
688 '"some public property"'
690 return assertContainsAll(response, expectedContent)
693 def getDataOperationRequest(operation, datastore) {
694 def dataOperationRequest = new DataOperationRequest()
695 def dataOperationDefinitions = new ArrayList()
696 dataOperationDefinitions.add(getDataOperationDefinition(operation, datastore))
697 dataOperationRequest.addOperationsItem(dataOperationDefinitions)
698 return dataOperationRequest
701 def getDataOperationDefinition(operation, datastore) {
702 def dataOperationDefinition = new DataOperationDefinition()
703 dataOperationDefinition.setOperation(operation)
704 dataOperationDefinition.setOperationId("operational-12")
705 dataOperationDefinition.setDatastore(datastore)
706 dataOperationDefinition.setOptions("some option")
707 dataOperationDefinition.setResourceIdentifier("some resource identifier")
708 dataOperationDefinition.addTargetIdsItem("some-cm-handle")
709 return dataOperationDefinition
713 def setupLogger = ((Logger) LoggerFactory.getLogger(NetworkCmProxyController.class))
714 setupLogger.setLevel(Level.DEBUG)
715 setupLogger.addAppender(logger)