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.api.models.CmResourceAddress
43 import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler
44 import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler
45 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
46 import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper
47 import org.onap.cps.ncmp.rest.mapper.DataOperationRequestMapper
48 import org.onap.cps.ncmp.rest.model.DataOperationDefinition
49 import org.onap.cps.ncmp.rest.model.DataOperationRequest
50 import org.onap.cps.ncmp.rest.util.DeprecationHelper
51 import org.onap.cps.spi.FetchDescendantsOption
52 import org.onap.cps.spi.model.ModuleDefinition
53 import org.onap.cps.spi.model.ModuleReference
54 import org.onap.cps.utils.JsonObjectMapper
55 import org.slf4j.LoggerFactory
56 import org.spockframework.spring.SpringBean
57 import org.springframework.beans.factory.annotation.Autowired
58 import org.springframework.beans.factory.annotation.Value
59 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
60 import org.springframework.http.HttpStatus
61 import org.springframework.http.MediaType
62 import org.springframework.test.web.servlet.MockMvc
63 import spock.lang.Shared
64 import spock.lang.Specification
66 import java.time.OffsetDateTime
67 import java.time.ZoneOffset
68 import java.time.format.DateTimeFormatter
70 import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.DataStores
71 import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.Operational
72 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
73 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
74 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
75 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
76 import static org.onap.cps.ncmp.api.impl.operations.OperationType.DELETE
77 import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH
78 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
79 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
80 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
81 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
82 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
83 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
84 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
85 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
87 @WebMvcTest(NetworkCmProxyController)
88 class NetworkCmProxyControllerSpec extends Specification {
94 NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
97 NetworkCmProxyQueryService mockNetworkCmProxyQueryService = Mock()
100 ObjectMapper objectMapper = new ObjectMapper()
103 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
106 NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
109 CmHandleStateMapper cmHandleStateMapper = Mappers.getMapper(CmHandleStateMapper)
112 DataOperationRequestMapper dataOperationRequestMapper = Mappers.getMapper(DataOperationRequestMapper)
115 Map<String, TrustLevel> trustLevelPerCmHandle = [:]
118 CpsNcmpTaskExecutor mockCpsTaskExecutor = Mock()
121 DeprecationHelper stubbedDeprecationHelper = Stub()
124 NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler = new NcmpCachedResourceRequestHandler(mockCpsTaskExecutor, mockNetworkCmProxyDataService, mockNetworkCmProxyQueryService)
127 NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler = new NcmpPassthroughResourceRequestHandler(mockCpsTaskExecutor, mockNetworkCmProxyDataService)
129 @Value('${rest.api.ncmp-base-path}/v1')
132 def requestBody = '{"some-key":"some-value"}'
134 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
138 def NO_REQUEST_ID = null
139 def NO_AUTH_HEADER = null
140 def TIMEOUT_FOR_TEST = 1234
142 def logger = Spy(ListAppender<ILoggingEvent>)
145 ncmpCachedResourceRequestHandler.notificationFeatureEnabled = true
146 ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMEOUT_FOR_TEST
147 ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = true
148 ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMEOUT_FOR_TEST
153 ((Logger) LoggerFactory.getLogger(EventsPublisher.class)).detachAndStopAllAppenders()
156 def 'Get Resource Data from pass-through operational.'() {
157 given: 'resource data url'
158 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational?resourceIdentifier=parent/child&options=(a=1,b=2)"
159 and: 'the expected cm resource address'
160 def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child')
161 when: 'get data resource request is performed'
162 def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
163 then: 'the NCMP data service is called with correct parameters'
164 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, '(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
165 and: 'response status is Ok'
166 assert response.status == HttpStatus.OK.value()
169 def 'Get Resource Data from ncmp-datastore:operational (cached) parameters handling with #scenario.'() {
170 given: 'resource data url'
171 def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational?resourceIdentifier=parent/child${additionalUrlParam}"
172 and: 'the expected cm resource address'
173 def expectedCmResourceAddress = new CmResourceAddress('ncmp-datastore:operational', 'h123', 'parent/child')
174 when: 'get data resource request is performed'
175 def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
176 then: 'the NCMP data service is called with correct parameters'
177 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, expectedIncludeDescendants)
178 and: 'response status is OK'
179 assert response.status == HttpStatus.OK.value()
180 where: 'the following parameters are used'
181 scenario | additionalUrlParam || expectedIncludeDescendants
182 'no additional param' | '' || OMIT_DESCENDANTS
183 'include descendants true' | '&include-descendants=true' || INCLUDE_ALL_DESCENDANTS
184 'include descendants TRUE' | '&include-descendants=true' || INCLUDE_ALL_DESCENDANTS
185 'include descendants false' | '&include-descendants=false' || OMIT_DESCENDANTS
186 'include descendants FALSE' | '&include-descendants=FALSE' || OMIT_DESCENDANTS
187 'options (ignored)' | '&options=(a-=1)' || OMIT_DESCENDANTS
190 def 'Execute (async) data operation to read data from dmi service.'() {
191 given: 'data operation url'
192 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
193 def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest('read', datastore.datastoreName))
194 when: 'post data operation request is performed'
195 def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)).andReturn().response
196 then: 'response status is Ok'
197 assert response.status == HttpStatus.OK.value()
198 and: 'async request id is generated'
199 assert response.contentAsString.contains('requestId')
200 then: 'the request is handled asynchronously'
201 1 * mockCpsTaskExecutor.executeTaskWithErrorHandling(*_)
202 where: 'the following data stores are used'
203 datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
206 def 'Execute (async) data operation with some validation error.'() {
207 given: 'data operation url'
208 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
209 def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest('read', 'invalid datastore'))
210 when: 'post data resource request is performed'
211 def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)).andReturn().response
212 then: 'response status is BAD_REQUEST'
213 assert response.status == HttpStatus.BAD_REQUEST.value()
216 def 'Get data operation resource data when notification feature is disabled for datastore: #datastore.'() {
217 given: 'data operation url'
218 def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
219 def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest("read", PASSTHROUGH_RUNNING.datastoreName))
220 ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = false
221 when: 'post data resource request is performed'
222 def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)
223 ).andReturn().response
224 then: 'response status is Ok'
225 assert response.status == HttpStatus.OK.value()
226 and: 'async request id is unavailable'
227 assert response.contentAsString == '{"status":"Asynchronous request is unavailable as notification feature is currently disabled."}'
230 def 'Query Resource Data from operational.'() {
231 given: 'the query resource data url'
232 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query?cps-path=/cps/path"
233 when: 'the query data resource request is performed'
234 def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
235 then: 'the NCMP query service is called with queryResourceDataOperationalForCmHandle'
236 1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle','/cps/path',FetchDescendantsOption.OMIT_DESCENDANTS)
237 and: 'response status is Ok'
238 assert response.status == HttpStatus.OK.value()
241 def 'Query Resource Data with unsupported datastore'() {
242 given: 'the query resource data url'
243 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running/query?cps-path=/cps/path"
244 when: 'the query data resource request is performed'
245 def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
246 then: 'a 400 BAD_REQUEST is returned for the unsupported datastore'
247 assert response.status == 400
248 and: 'the error message is that the datastore is not supported'
249 assert response.contentAsString.contains("ncmp-datastore:passthrough-running is not supported")
252 def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
253 given: 'resource data url'
254 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=$resourceIdentifier&options=(a=1,b=2)"
255 and: 'ncmp service returns json object'
256 def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', resourceIdentifier)
257 mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> '{valid-json}'
258 when: 'get data resource request is performed'
259 def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
260 then: 'response status is Ok'
261 response.status == HttpStatus.OK.value()
262 and: 'response contains valid object body'
263 response.getContentAsString() == '{valid-json}'
264 where: 'tokens are used in the resource identifier parameter'
265 scenario | resourceIdentifier
266 '/' | 'id/with/slashes'
271 '? needs to be encoded as %3F' | 'idWith%3F'
274 def 'Update resource data from pass-through running.'() {
275 given: 'update resource data url'
276 def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
277 when: 'update data resource request is performed'
278 def response = mvc.perform(put(updateUrl).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)).andReturn().response
279 then: 'ncmp service method to update resource is called'
280 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle','parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
281 and: 'the response status is OK'
282 assert response.status == HttpStatus.OK.value()
285 def 'Create Resource Data from pass-through running with #scenario.'() {
286 given: 'resource data url'
287 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
288 when: 'create resource request is performed'
289 def response = mvc.perform(post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)).andReturn().response
290 then: 'ncmp service method to create resource called'
291 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
292 and: 'resource is created'
293 assert response.status == HttpStatus.CREATED.value()
296 def 'Get module references for the given dataspace and cm handle.'() {
297 given: 'get module references url'
298 def getUrl = "$ncmpBasePathV1/ch/some-cmhandle/modules"
299 when: 'get module resource request is performed'
300 def response = mvc.perform(get(getUrl)).andReturn().response
301 then: 'ncmp service method to get yang resource module references is called'
302 mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle') >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')]
303 and: 'response contains an array with the module name and revision'
304 response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]'
305 and: 'response returns an OK http code'
306 assert response.status == HttpStatus.OK.value()
309 def 'Retrieve cm handles.'() {
310 given: 'an endpoint and json data'
311 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
312 String jsonString = TestUtils.getResourceFileContent('cmhandle-search.json')
313 and: 'the service method is invoked with module names and returns two cm handles'
314 def cmHandle1 = new NcmpServiceCmHandle()
315 cmHandle1.cmHandleId = 'ch-1'
316 cmHandle1.publicProperties = [color: 'yellow']
317 def cmHandle2 = new NcmpServiceCmHandle()
318 cmHandle2.cmHandleId = 'ch-2'
319 cmHandle2.publicProperties = [color: 'green']
320 cmHandle2.alternateId = 'someAlternateId'
321 cmHandle2.moduleSetTag = 'someModuleSetTag'
322 cmHandle2.dataProducerIdentifier = 'someDataProducerIdentifier'
323 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandle1, cmHandle2]
324 and: 'map for trust level per cmHandle has value for only one cm handle'
325 trustLevelPerCmHandle.put('ch-1', TrustLevel.NONE)
326 when: 'the searches api is invoked'
327 def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(jsonString)).andReturn().response
328 then: 'response status returns OK'
329 assert response.status == HttpStatus.OK.value()
330 and: 'the expected response content is returned'
331 assert 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"}]'
334 def 'Get complete Cm Handle details by Cm Handle id.'() {
335 given: 'an endpoint and a cm handle'
336 def cmHandleDetailsEndpoint = "$ncmpBasePathV1/ch/some-cm-handle"
337 and: 'an existing ncmp service cm handle'
338 def cmHandleId = 'some-cm-handle'
339 def dmiProperties = [prop: 'some DMI property']
340 def publicProperties = ["public prop": 'some public property']
341 def compositeState = compositeStateTestObject()
342 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
343 and: 'the service method is invoked with the cm handle id'
344 1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('some-cm-handle') >> ncmpServiceCmHandle
345 and: 'map for trust level per cmHandle has values'
346 trustLevelPerCmHandle.get('some-cm-handle') >> { TrustLevel.COMPLETE }
347 when: 'the cm handle details api is invoked'
348 def response = mvc.perform(
349 get(cmHandleDetailsEndpoint)).andReturn().response
350 then: 'the correct response is returned'
351 response.status == HttpStatus.OK.value()
352 and: 'the response contains the public properties'
353 assertContainsPublicProperties(response)
354 and: 'the response contains the cm handle state'
355 assertContainsState(response)
356 and: 'the content does not contain dmi properties'
357 assert !response.contentAsString.contains("some DMI property")
360 def 'Get Cm Handle public properties by Cm Handle id.'() {
361 given: 'a cm handle properties endpoint'
362 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/properties"
363 and: 'some cm handle public properties'
364 def publicProperties = ['public prop': 'some public property']
365 and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
366 1 * mockNetworkCmProxyDataService.getCmHandlePublicProperties('some-cm-handle') >> publicProperties
367 when: 'the cm handle properties api is invoked'
368 def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response
369 then: 'the correct response is returned'
370 assert response.status == HttpStatus.OK.value()
371 and: 'the response contains the public properties'
372 assertContainsPublicProperties(response)
375 def 'Get Cm Handle composite state by Cm Handle id.'() {
376 given: 'a cm handle state endpoint'
377 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/state"
378 and: 'some cm handle composite state'
379 def compositeState = compositeStateTestObject()
380 and: 'the service method is invoked with the cm handle id returning the cm handle composite state'
381 1 * mockNetworkCmProxyDataService.getCmHandleCompositeState('some-cm-handle') >> compositeState
382 when: 'the cm handle state api is invoked'
383 def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response
384 then: 'the correct response is returned'
385 response.status == HttpStatus.OK.value()
386 and: 'the response contains the cm handle state'
387 assert assertContainsState(response)
390 def 'Call execute cm handle searches with unrecognized condition name.'() {
391 given: 'an endpoint and json data'
392 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
393 String jsonString = TestUtils.getResourceFileContent('invalid-cmhandle-search.json')
394 and: 'the service method is invoked with module names and returns two cm handles'
395 def cmHandel1 = new NcmpServiceCmHandle()
396 cmHandel1.cmHandleId = 'ch-1'
397 cmHandel1.publicProperties = [color: 'yellow']
398 def cmHandel2 = new NcmpServiceCmHandle()
399 cmHandel2.cmHandleId = 'ch-2'
400 cmHandel2.publicProperties = [color: 'green']
401 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandel1, cmHandel2]
402 and: 'map for trust level per cmHandle has values'
403 trustLevelPerCmHandle.put('ch-1', TrustLevel.COMPLETE)
404 trustLevelPerCmHandle.put('ch-2', TrustLevel.NONE)
405 when: 'the searches api is invoked'
406 def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(jsonString)).andReturn().response
407 then: 'an empty cm handle identifier is returned'
408 assert 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}]'
411 def 'Query for cm handles matching query parameters'() {
412 given: 'an endpoint and json data'
413 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
414 and: 'the service method is invoked with module names and returns cm handle ids'
415 1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['ch-1', 'ch-2']
416 when: 'the searches api is invoked'
417 def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content('{}')).andReturn().response
418 then: 'cm handle ids are returned'
419 assert response.contentAsString == '["ch-1","ch-2"]'
422 def 'Query for cm handles with invalid request payload'() {
423 when: 'the searches api is invoked'
424 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
425 def invalidInputData = '{invalidJson}'
426 def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(invalidInputData)).andReturn().response
427 then: 'BAD_REQUEST is returned'
428 assert response.getStatus() == 400
431 def 'Patch resource data in pass-through running datastore.'() {
432 given: 'patch resource data url'
433 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
434 when: 'patch data resource request is performed'
435 def response = mvc.perform(patch(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content(requestBody)).andReturn().response
436 then: 'ncmp service method to update resource is called'
437 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
438 and: 'the response status is OK'
439 assert response.status == HttpStatus.OK.value()
442 def 'Delete resource data in pass-through running datastore.'() {
443 given: 'delete resource data url'
444 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
445 when: 'delete data resource request is performed'
446 def response = mvc.perform(delete(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
447 then: 'the ncmp service method to delete resource is called (with null as body)'
448 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', DELETE, null, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
449 and: 'the response is No Content'
450 assert response.status == HttpStatus.NO_CONTENT.value()
453 def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
454 given: 'resource data url'
455 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
456 when: 'get data resource request is performed'
457 def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON_VALUE)).andReturn().response
458 then: 'async request id is generated'
459 assert response.contentAsString.contains("requestId")
460 where: 'the following parameters are used'
461 scenario | datastoreInUrl
462 ':passthrough-operational' | 'passthrough-operational'
463 ':passthrough-running' | 'passthrough-running'
466 def 'Getting module definitions for a module'() {
467 when: 'get module definition request is performed with module name'
468 def response = mvc.perform(get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=sampleModuleName")).andReturn().response
469 then: 'ncmp service method is invoked with correct parameters'
470 mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', 'sampleModuleName', _)
471 >> [new ModuleDefinition('sampleModuleName', '2021-10-03','module sampleModuleName{ sample module content }')]
472 and: 'response contains an array with the module name, revision and content'
473 response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]'
474 and: 'response returns an OK http code'
475 assert response.status == HttpStatus.OK.value()
478 def 'Getting module definitions filtering on #scenario'() {
479 when: 'get module definition request is performed'
480 def response = mvc.perform(
481 get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=" + moduleName + "&revision=" + revision))
482 .andReturn().response
483 then: 'ncmp service method to get definitions by cm handle is invoked when needed'
484 numberOfCallsToByCmHandleId * mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleId('some-cmhandle') >> []
485 and: 'ncmp service method to get definitions by module is invoked when needed'
486 numberOfCallsToByModule * mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', moduleName, revision) >> []
487 and: 'response returns an OK http code'
488 response.status == HttpStatus.OK.value()
489 and: 'the correct message is logged when needed'
490 if (expectLogWarning) {
491 def lastLoggingEvent = logger.list[0]
492 assert lastLoggingEvent.level == Level.WARN
493 assert lastLoggingEvent.formattedMessage.contains('Ignoring revision')
495 where: 'following parameters are used'
496 scenario | moduleName | revision || numberOfCallsToByCmHandleId | numberOfCallsToByModule | expectLogWarning
497 'module name' | 'some-module' | '' || 0 | 1 | false
498 'module name and revision' | 'some-module' | 'some-revision' || 0 | 1 | false
499 'no filtering' | '' | '' || 1 | 0 | false
500 'only revision' | '' | 'some-revision' || 1 | 0 | true
503 def 'Set the data sync enabled based on the cm handle id and the data sync flag is #scenario'() {
504 when: 'the set data sync enabled request is invoked'
505 def response = mvc.perform(
506 put("$ncmpBasePathV1/ch/some-cm-handle-id/data-sync?dataSyncEnabled=" + dataSyncEnabledFlag))
507 .andReturn().response
508 then: 'method to set data sync enabled is called'
509 1 * mockNetworkCmProxyDataService.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
510 and: 'the response returns an OK http code'
511 response.status == HttpStatus.OK.value()
512 where: 'the following parameters are used'
513 scenario | dataSyncEnabledFlag
518 def 'Get Resource Data from operational with or without descendants'() {
519 given: 'resource data url with descendants #enabled'
520 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational?resourceIdentifier=parent/child&include-descendants=${booleanValue}"
521 and: 'the expected target'
522 def expectedCmResourceAddress = new CmResourceAddress(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child')
523 when: 'get data resource request is performed'
524 def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
525 then: 'the NCMP data service is called with getResourceDataOperational with #descendantsOption'
526 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, descendantsOption)
527 and: 'response status is Ok'
528 assert response.status == HttpStatus.OK.value()
529 where: 'the following parameters are used'
530 booleanValue | descendantsOption
531 false | OMIT_DESCENDANTS
532 true | INCLUDE_ALL_DESCENDANTS
535 def 'Attempt execute #operation rest operation on resource data with #scenario'() {
536 given: 'resource data url'
537 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/${datastoreInUrl}?resourceIdentifier=parent/child"
538 when: 'selected request for data resource is performed on url'
539 def response = mvc.perform(
540 executeRestOperation(operation, url))
541 .andReturn().response
542 then: 'the response status is as expected'
543 assert response.status == HttpStatus.BAD_REQUEST.value()
544 and: 'the response is as expected'
545 assert response.getContentAsString().contains(datastoreInUrl)
546 where: 'the following parameters are used'
547 scenario | operation | datastoreInUrl
548 'unsupported datastore' | 'POST' | 'ncmp-datastore:operational'
549 'invalid datastore' | 'POST' | 'invalid'
550 'unsupported datastore' | 'PUT' | 'ncmp-datastore:operational'
551 'invalid datastore' | 'PUT' | 'invalid'
552 'unsupported datastore' | 'PATCH' | 'ncmp-datastore:operational'
553 'invalid datastore' | 'PATCH' | 'invalid'
554 'unsupported datastore' | 'DELETE' | 'ncmp-datastore:operational'
555 'invalid datastore' | 'DELETE' | 'invalid'
558 def executeRestOperation(operation, url) {
559 if (operation == 'POST') {
560 return post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
562 if (operation == 'PUT') {
563 return put(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
565 if (operation == 'PATCH') {
566 return patch(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
568 if (operation == 'DELETE') {
569 return delete(url).contentType(MediaType.APPLICATION_JSON_VALUE)
575 .operationalDataStore(Operational.builder()
576 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
577 .lastSyncTime(formattedDateAndTime.toString()).build()).build()
580 def compositeStateTestObject() {
581 new CompositeState(cmHandleState: CmHandleState.ADVISED,
582 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
583 lastUpdateTime: formattedDateAndTime.toString(),
584 dataSyncEnabled: false,
585 dataStores: dataStores())
588 def assertContainsAll(response, assertContent) {
589 assertContent.forEach(string -> { assert (response.contentAsString.contains(string)) })
593 def assertContainsState(response) {
594 def expectedContent = [
596 '"cmHandleState":"ADVISED"',
597 '"lockReason":{"reason":"MODULE_SYNC_FAILED","details":"lock details"}',
598 '"lastUpdateTime":"2022-12-31T20:30:40.000+0000"',
599 '"dataSyncEnabled":false',
602 '"syncState":"NONE_REQUESTED"',
603 '"lastSyncTime":"2022-12-31T20:30:40.000+0000"',
606 return assertContainsAll(response, expectedContent)
609 def assertContainsPublicProperties(response) {
610 def expectedContent = [
611 '"publicCmHandleProperties":',
613 '"some public property"'
615 return assertContainsAll(response, expectedContent)
618 def getDataOperationRequest(operation, datastore) {
619 def dataOperationRequest = new DataOperationRequest()
620 def dataOperationDefinitions = new ArrayList()
621 dataOperationDefinitions.add(getDataOperationDefinition(operation, datastore))
622 dataOperationRequest.addOperationsItem(dataOperationDefinitions)
623 return dataOperationRequest
626 def getDataOperationDefinition(operation, datastore) {
627 def dataOperationDefinition = new DataOperationDefinition()
628 dataOperationDefinition.setOperation(operation)
629 dataOperationDefinition.setOperationId("operational-12")
630 dataOperationDefinition.setDatastore(datastore)
631 dataOperationDefinition.setOptions("some option")
632 dataOperationDefinition.setResourceIdentifier("some resource identifier")
633 dataOperationDefinition.addTargetIdsItem("some-cm-handle")
634 return dataOperationDefinition
638 def setupLogger = ((Logger) LoggerFactory.getLogger(NetworkCmProxyController.class))
639 setupLogger.setLevel(Level.DEBUG)
640 setupLogger.addAppender(logger)