2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021 Pantheon.tech
4 * Modifications Copyright (C) 2021 highstreet technologies GmbH
5 * Modifications Copyright (C) 2021-2022 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 org.mapstruct.factory.Mappers
27 import org.onap.cps.ncmp.api.inventory.CmHandleState
28 import org.onap.cps.ncmp.api.inventory.CompositeState
29 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
30 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
31 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
32 import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper
33 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
34 import org.onap.cps.ncmp.rest.util.DeprecationHelper
35 import org.onap.cps.spi.model.ModuleDefinition
36 import spock.lang.Shared
38 import java.time.OffsetDateTime
39 import java.time.ZoneOffset
40 import java.time.format.DateTimeFormatter
42 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.PATCH
43 import static org.onap.cps.ncmp.api.inventory.CompositeState.DataStores
44 import static org.onap.cps.ncmp.api.inventory.CompositeState.Operational
45 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
46 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
47 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
48 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
49 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
50 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.CREATE
51 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.UPDATE
52 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.DELETE
54 import com.fasterxml.jackson.databind.ObjectMapper
55 import org.onap.cps.TestUtils
56 import org.onap.cps.spi.model.ModuleReference
57 import org.onap.cps.utils.JsonObjectMapper
58 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
59 import org.spockframework.spring.SpringBean
60 import org.springframework.beans.factory.annotation.Autowired
61 import org.springframework.beans.factory.annotation.Value
62 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
63 import org.springframework.http.HttpStatus
64 import org.springframework.http.MediaType
65 import org.springframework.test.web.servlet.MockMvc
66 import spock.lang.Specification
68 @WebMvcTest(NetworkCmProxyController)
69 class NetworkCmProxyControllerSpec extends Specification {
75 NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
78 ObjectMapper objectMapper = new ObjectMapper()
81 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
84 NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
87 CmHandleStateMapper cmHandleStateMapper = Mappers.getMapper(CmHandleStateMapper)
90 CpsNcmpTaskExecutor spiedCpsTaskExecutor = Spy()
93 DeprecationHelper stubbedDeprecationHelper = Stub()
95 @Value('${rest.api.ncmp-base-path}/v1')
98 def requestBody = '{"some-key":"some-value"}'
102 def NO_REQUEST_ID = null
104 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
105 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
107 def 'Get Resource Data from pass-through operational.'() {
108 given: 'resource data url'
109 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
110 "?resourceIdentifier=parent/child&options=(a=1,b=2)"
111 when: 'get data resource request is performed'
112 def response = mvc.perform(
114 .contentType(MediaType.APPLICATION_JSON)
115 ).andReturn().response
116 then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle'
117 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
122 and: 'response status is Ok'
123 response.status == HttpStatus.OK.value()
126 def 'Get Resource Data from #datastoreInUrl with #scenario.'() {
127 given: 'resource data url'
128 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
129 "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
130 when: 'get data resource request is performed'
131 def response = mvc.perform(
132 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
133 then: 'task executor is called appropriate number of times'
134 expectedNumberOfExecutorExecutions * spiedCpsTaskExecutor.executeTask(_, 2000)
135 and: 'response status is expected'
136 response.status == HttpStatus.OK.value()
137 where: 'the following parameters are used'
138 scenario | datastoreInUrl | topicQueryParam || expectedTopicName | expectedNumberOfExecutorExecutions
139 'url with valid topic' | 'passthrough-operational' | '&topic=my-topic-name' || 'my-topic-name' | 1
140 'no topic in url' | 'passthrough-operational' | '' || NO_TOPIC | 0
141 'null topic in url' | 'passthrough-operational' | '&topic=null' || 'null' | 1
142 'url with valid topic' | 'passthrough-running' | '&topic=my-topic-name' || 'my-topic-name' | 1
143 'no topic in url' | 'passthrough-running' | '' || NO_TOPIC | 0
144 'null topic in url' | 'passthrough-running' | '&topic=null' || 'null' | 1
147 def 'Fail to get Resource Data from #datastoreInUrl when #scenario.'() {
148 given: 'resource data url'
149 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
150 "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
151 when: 'get data resource request is performed'
152 def response = mvc.perform(
153 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
154 then: 'abad request is returned'
155 response.status == HttpStatus.BAD_REQUEST.value()
156 where: 'the following parameters are used'
157 scenario | datastoreInUrl | topicQueryParam
158 'empty topic in url' | 'passthrough-operational' | '&topic=\"\"'
159 'missing topic in url' | 'passthrough-operational' | '&topic='
160 'blank topic value in url' | 'passthrough-operational' | '&topic=\" \"'
161 'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#'
162 'empty topic in url' | 'passthrough-running' | '&topic=\"\"'
163 'missing topic in url' | 'passthrough-running' | '&topic='
164 'blank topic value in url' | 'passthrough-running' | '&topic=\" \"'
165 'invalid non-empty topic value in url' | 'passthrough-running' | '&topic=1_5_*_#'
168 def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
169 given: 'resource data url'
170 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
171 "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)"
172 and: 'ncmp service returns json object'
173 mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
177 NO_REQUEST_ID) >> '{valid-json}'
178 when: 'get data resource request is performed'
179 def response = mvc.perform(
181 .contentType(MediaType.APPLICATION_JSON)
182 ).andReturn().response
183 then: 'response status is Ok'
184 response.status == HttpStatus.OK.value()
185 and: 'response contains valid object body'
186 response.getContentAsString() == '{valid-json}'
187 where: 'tokens are used in the resource identifier parameter'
188 scenario | resourceIdentifier
189 '/' | 'id/with/slashes'
194 '? needs to be encoded as %3F' | 'idWith%3F'
197 def 'Update resource data from pass-through running.' () {
198 given: 'update resource data url'
199 def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
200 "?resourceIdentifier=parent/child"
201 when: 'update data resource request is performed'
202 def response = mvc.perform(
204 .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
205 ).andReturn().response
206 then: 'ncmp service method to update resource is called'
207 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
208 'parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8')
209 and: 'the response status is OK'
210 response.status == HttpStatus.OK.value()
213 def 'Create Resource Data from pass-through running with #scenario.' () {
214 given: 'resource data url'
215 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
216 "?resourceIdentifier=parent/child"
217 def requestBody = '{"some-key":"some-value"}'
218 when: 'create resource request is performed'
219 def response = mvc.perform(
221 .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
222 ).andReturn().response
223 then: 'ncmp service method to create resource called'
224 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
225 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8')
226 and: 'resource is created'
227 response.status == HttpStatus.CREATED.value()
230 def 'Get module references for the given dataspace and cm handle.' () {
231 given: 'get module references url'
232 def getUrl = "$ncmpBasePathV1/ch/some-cmhandle/modules"
233 when: 'get module resource request is performed'
234 def response =mvc.perform(get(getUrl)).andReturn().response
235 then: 'ncmp service method to get yang resource module references is called'
236 mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle')
237 >> [new ModuleReference(moduleName: 'some-name1',revision: '2021-10-03')]
238 and: 'response contains an array with the module name and revision'
239 response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]'
240 and: 'response returns an OK http code'
241 response.status == HttpStatus.OK.value()
244 def 'Retrieve cm handles.'() {
245 given: 'an endpoint and json data'
246 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
247 String jsonString = TestUtils.getResourceFileContent('cmhandle-search.json')
248 and: 'the service method is invoked with module names and returns two cm handles'
249 def cmHandle1 = new NcmpServiceCmHandle()
250 cmHandle1.cmHandleId = 'some-cmhandle-id1'
251 cmHandle1.publicProperties = [color:'yellow']
252 def cmHandle2 = new NcmpServiceCmHandle()
253 cmHandle2.cmHandleId = 'some-cmhandle-id2'
254 cmHandle2.publicProperties = [color:'green']
255 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandle1, cmHandle2]
256 when: 'the searches api is invoked'
257 def response = mvc.perform(post(searchesEndpoint)
258 .contentType(MediaType.APPLICATION_JSON)
259 .content(jsonString)).andReturn().response
260 then: 'response status returns OK'
261 response.status == HttpStatus.OK.value()
262 and: 'the expected response content is returned'
263 response.contentAsString == '[{"cmHandle":"some-cmhandle-id1","publicCmHandleProperties":[{"color":"yellow"}],"state":null},{"cmHandle":"some-cmhandle-id2","publicCmHandleProperties":[{"color":"green"}],"state":null}]'
266 def 'Get complete Cm Handle details by Cm Handle id.'() {
267 given: 'an endpoint and a cm handle'
268 def cmHandleDetailsEndpoint = "$ncmpBasePathV1/ch/some-cm-handle"
269 and: 'an existing ncmp service cm handle'
270 def cmHandleId = 'some-cm-handle'
271 def dmiProperties = [ prop:'some DMI property' ]
272 def publicProperties = [ "public prop":'some public property' ]
273 def compositeState = compositeStateTestObject()
274 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
275 and: 'the service method is invoked with the cm handle id'
276 1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('some-cm-handle') >> ncmpServiceCmHandle
277 when: 'the cm handle details api is invoked'
278 def response = mvc.perform(get(cmHandleDetailsEndpoint)).andReturn().response
279 then: 'the correct response is returned'
280 response.status == HttpStatus.OK.value()
281 and: 'the response contains the public properties'
282 assertContainsPublicProperties(response)
283 and: 'the response contains the cm handle state'
284 assertContainsState(response)
285 and: 'the content does not contain dmi properties'
286 !response.contentAsString.contains("some DMI property")
289 def 'Get Cm Handle public properties by Cm Handle id.' () {
290 given: 'a cm handle properties endpoint'
291 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/properties"
292 and: 'some cm handle public properties'
293 def publicProperties = [ 'public prop':'some public property' ]
294 and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
295 1 * mockNetworkCmProxyDataService.getCmHandlePublicProperties('some-cm-handle') >> publicProperties
296 when: 'the cm handle properties api is invoked'
297 def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response
298 then: 'the correct response is returned'
299 response.status == HttpStatus.OK.value()
300 and: 'the response contains the public properties'
301 assertContainsPublicProperties(response)
304 def 'Get Cm Handle composite state by Cm Handle id.' () {
305 given: 'a cm handle state endpoint'
306 def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/state"
307 and: 'some cm handle composite state'
308 def compositeState = compositeStateTestObject()
309 and: 'the service method is invoked with the cm handle id returning the cm handle composite state'
310 1 * mockNetworkCmProxyDataService.getCmHandleCompositeState('some-cm-handle') >> compositeState
311 when: 'the cm handle state api is invoked'
312 def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response
313 then: 'the correct response is returned'
314 response.status == HttpStatus.OK.value()
315 and: 'the response contains the cm handle state'
316 assertContainsState(response)
319 def 'Call execute cm handle searches with unrecognized condition name.'() {
320 given: 'an endpoint and json data'
321 def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
322 String jsonString = TestUtils.getResourceFileContent('invalid-cmhandle-search.json')
323 and: 'the service method is invoked with module names and returns two cm handles'
324 def cmHandel1 = new NcmpServiceCmHandle()
325 cmHandel1.cmHandleId = 'some-cmhandle-id1'
326 cmHandel1.publicProperties = [color:'yellow']
327 def cmHandel2 = new NcmpServiceCmHandle()
328 cmHandel2.cmHandleId = 'some-cmhandle-id2'
329 cmHandel2.publicProperties = [color:'green']
330 mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandel1, cmHandel2]
331 when: 'the searches api is invoked'
332 def response = mvc.perform(post(searchesEndpoint)
333 .contentType(MediaType.APPLICATION_JSON)
334 .content(jsonString)).andReturn().response
335 then: 'an empty cm handle identifier is returned'
336 response.contentAsString == '[{"cmHandle":"some-cmhandle-id1","publicCmHandleProperties":[{"color":"yellow"}],"state":null},{"cmHandle":"some-cmhandle-id2","publicCmHandleProperties":[{"color":"green"}],"state":null}]'
339 def 'Query for cm handles matching query parameters'() {
340 given: 'an endpoint and json data'
341 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
342 and: 'the service method is invoked with module names and returns cm handle ids'
343 1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2']
344 when: 'the searches api is invoked'
345 def response = mvc.perform(post(searchesEndpoint)
346 .contentType(MediaType.APPLICATION_JSON)
347 .content('{}')).andReturn().response
348 then: 'cm handle ids are returned'
349 response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]'
352 def 'Query for cm handles with invalid request payload'() {
353 when: 'the searches api is invoked'
354 def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
355 def invalidInputData = '{invalidJson}'
356 def response = mvc.perform(post(searchesEndpoint)
357 .contentType(MediaType.APPLICATION_JSON)
358 .content(invalidInputData)).andReturn().response
359 then: 'BAD_REQUEST is returned'
360 response.getStatus() == 400
363 def 'Patch resource data in pass-through running datastore.' () {
364 given: 'patch resource data url'
365 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
366 "?resourceIdentifier=parent/child"
367 when: 'patch data resource request is performed'
368 def response = mvc.perform(
370 .contentType(MediaType.APPLICATION_JSON)
371 .accept(MediaType.APPLICATION_JSON).content(requestBody)
372 ).andReturn().response
373 then: 'ncmp service method to update resource is called'
374 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
375 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8')
376 and: 'the response status is OK'
377 response.status == HttpStatus.OK.value()
380 def 'Delete resource data in pass-through running datastore.' () {
381 given: 'delete resource data url'
382 def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
383 "?resourceIdentifier=parent/child"
384 when: 'delete data resource request is performed'
385 def response = mvc.perform(
386 delete(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
387 then: 'the ncmp service method to delete resource is called (with null as body)'
388 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
389 'parent/child', DELETE, null, 'application/json;charset=UTF-8')
390 and: 'the response is No Content'
391 response.status == HttpStatus.NO_CONTENT.value()
394 def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
395 given: 'resource data url'
396 def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
397 "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
398 when: 'get data resource request is performed'
399 def response = mvc.perform(
401 .contentType(MediaType.APPLICATION_JSON)
402 .accept(MediaType.APPLICATION_JSON_VALUE)
403 ).andReturn().response
404 then: 'async request id is generated'
405 assert response.contentAsString.contains("requestId")
406 where: 'the following parameters are used'
407 scenario | datastoreInUrl
408 ':passthrough-operational' | 'passthrough-operational'
409 ':passthrough-running' | 'passthrough-running'
412 def 'Get module definitions based on cmHandleId.' () {
413 when: 'get module definition request is performed'
414 def response = mvc.perform(get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions"))
415 .andReturn().response
416 then: 'ncmp service method to get module definitions is called'
417 mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleId('some-cmhandle')
418 >> [new ModuleDefinition('sampleModuleName', '2021-10-03',
419 'module sampleModuleName{ sample module content }')]
420 and: 'response contains an array with the module name, revision and content'
421 response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]'
422 and: 'response returns an OK http code'
423 response.status == HttpStatus.OK.value()
428 .operationalDataStore(Operational.builder()
429 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
430 .lastSyncTime(formattedDateAndTime.toString()).build()).build()
433 def compositeStateTestObject() {
434 new CompositeState(cmHandleState: CmHandleState.ADVISED,
435 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
436 lastUpdateTime: formattedDateAndTime.toString(),
437 dataSyncEnabled: false,
438 dataStores: dataStores())
441 def assertContainsAll(response, assertContent) {
442 assertContent.forEach( string -> { assert(response.contentAsString.contains(string)) })
446 def assertContainsState(response) {
447 def expectedContent = [
449 '"cmHandleState":"ADVISED"',
450 '"reason":"LOCKED_MISBEHAVING"',
451 '"details":"lock details"',
452 '"lastUpdateTime":"2022-12-31T20:30:40.000+0000"',
453 '"dataSyncEnabled":false',
456 '"state":"NONE_REQUESTED"',
457 '"lastSyncTime":"2022-12-31T20:30:40.000+0000"',
460 return assertContainsAll(response, expectedContent)
463 def assertContainsPublicProperties(response) {
464 def expectedContent = [
465 '"publicCmHandleProperties":' ,
467 '"some public property"'
469 return assertContainsAll(response, expectedContent)