2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2023 Nordix Foundation
4 * Modifications Copyright (C) 2021-2022 Bell Canada
5 * ================================================================================
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
22 package org.onap.cps.ncmp.dmi.rest.controller
24 import org.onap.cps.ncmp.dmi.TestUtils
25 import org.onap.cps.ncmp.dmi.config.WebSecurityConfig
26 import org.onap.cps.ncmp.dmi.exception.DmiException
27 import org.onap.cps.ncmp.dmi.exception.ModuleResourceNotFoundException
28 import org.onap.cps.ncmp.dmi.exception.ModulesNotFoundException
29 import org.onap.cps.ncmp.dmi.model.ModuleSet
30 import org.onap.cps.ncmp.dmi.model.ModuleSetSchemasInner
31 import org.onap.cps.ncmp.dmi.model.YangResource
32 import org.onap.cps.ncmp.dmi.model.YangResources
33 import org.onap.cps.ncmp.dmi.notifications.async.AsyncTaskExecutor
34 import org.onap.cps.ncmp.dmi.notifications.async.DmiAsyncRequestResponseEventProducer
35 import org.onap.cps.ncmp.dmi.service.DmiService
36 import org.onap.cps.ncmp.dmi.service.model.ModuleReference
37 import org.spockframework.spring.SpringBean
38 import org.springframework.beans.factory.annotation.Autowired
39 import org.springframework.beans.factory.annotation.Value
40 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
41 import org.springframework.context.annotation.Import
42 import org.springframework.http.HttpStatus
43 import org.springframework.http.MediaType
44 import org.springframework.kafka.core.KafkaTemplate
45 import org.springframework.security.test.context.support.WithMockUser
46 import org.springframework.test.web.servlet.MockMvc
47 import spock.lang.Specification
49 import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.CREATE
50 import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.DELETE
51 import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.PATCH
52 import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.READ
53 import static org.onap.cps.ncmp.dmi.model.DataAccessRequest.OperationEnum.UPDATE
54 import static org.springframework.http.HttpStatus.BAD_REQUEST
55 import static org.springframework.http.HttpStatus.CREATED
56 import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR
57 import static org.springframework.http.HttpStatus.NO_CONTENT
58 import static org.springframework.http.HttpStatus.OK
59 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
61 @Import(WebSecurityConfig)
62 @WebMvcTest(DmiRestController.class)
64 class DmiRestControllerSpec extends Specification {
70 DmiService mockDmiService = Mock()
73 DmiAsyncRequestResponseEventProducer cpsAsyncRequestResponseEventProducer = new DmiAsyncRequestResponseEventProducer(Mock(KafkaTemplate))
76 AsyncTaskExecutor asyncTaskExecutor = new AsyncTaskExecutor(cpsAsyncRequestResponseEventProducer)
78 @Value('${rest.api.dmi-base-path}/v1')
81 def 'Get all modules.'() {
82 given: 'URL for getting all modules and some request data'
83 def getModuleUrl = "$basePathV1/ch/node1/modules"
84 def someValidJson = '{}'
85 and: 'DMI service returns some module'
86 def moduleSetSchema = new ModuleSetSchemasInner(namespace:'some-namespace',
87 moduleName:'some-moduleName',
88 revision:'some-revision')
89 def moduleSetSchemasList = [moduleSetSchema] as List<ModuleSetSchemasInner>
90 def moduleSet = new ModuleSet()
91 moduleSet.schemas(moduleSetSchemasList)
92 mockDmiService.getModulesForCmHandle('node1') >> moduleSet
93 when: 'the request is posted'
94 def response = mvc.perform(post(getModuleUrl)
95 .contentType(MediaType.APPLICATION_JSON).content(someValidJson))
98 response.status == OK.value()
99 and: 'the response content matches the result from the DMI service'
100 response.getContentAsString() == '{"schemas":[{"moduleName":"some-moduleName","revision":"some-revision","namespace":"some-namespace"}]}'
103 def 'Get all modules with exception handling of #scenario.'() {
104 given: 'URL for getting all modules and some request data'
105 def getModuleUrl = "$basePathV1/ch/node1/modules"
106 def someValidJson = '{}'
107 and: 'a #exception is thrown during the process'
108 mockDmiService.getModulesForCmHandle('node1') >> { throw exception }
109 when: 'the request is posted'
110 def response = mvc.perform( post(getModuleUrl)
111 .contentType(MediaType.APPLICATION_JSON).content(someValidJson))
112 .andReturn().response
113 then: 'response status is #expectedResponse'
114 response.status == expectedResponse.value()
115 where: 'the scenario is #scenario'
116 scenario | exception || expectedResponse
117 'dmi service exception' | new DmiException('','') || HttpStatus.INTERNAL_SERVER_ERROR
118 'no modules found' | new ModulesNotFoundException('','') || HttpStatus.NOT_FOUND
119 'any other runtime exception' | new RuntimeException() || HttpStatus.INTERNAL_SERVER_ERROR
120 'runtime exception with cause' | new RuntimeException('', new RuntimeException()) || HttpStatus.INTERNAL_SERVER_ERROR
123 def 'Register given list.'() {
124 given: 'register cm handle url and cmHandles'
125 def registerCmhandlesPost = "${basePathV1}/inventory/cmHandles"
126 def cmHandleJson = '{"cmHandles":["node1", "node2"]}'
127 when: 'the request is posted'
128 def response = mvc.perform(
129 post(registerCmhandlesPost)
130 .contentType(MediaType.APPLICATION_JSON)
131 .content(cmHandleJson)
132 ).andReturn().response
133 then: 'register cm handles in dmi service is invoked with correct parameters'
134 1 * mockDmiService.registerCmHandles(_ as List<String>)
135 and: 'response status is created'
136 response.status == CREATED.value()
139 def 'register cm handles called with empty content.'() {
140 given: 'register cm handle url and empty json'
141 def registerCmhandlesPost = "${basePathV1}/inventory/cmHandles"
142 def emptyJson = '{"cmHandles":[]}'
143 when: 'the request is posted'
144 def response = mvc.perform(
145 post(registerCmhandlesPost).contentType(MediaType.APPLICATION_JSON)
147 ).andReturn().response
148 then: 'response status is "bad request"'
149 response.status == BAD_REQUEST.value()
150 and: 'dmi service is not called'
151 0 * mockDmiService.registerCmHandles(_)
154 def 'Retrieve module resources.'() {
155 given: 'URL to get module resources'
156 def getModulesEndpoint = "$basePathV1/ch/some-cm-handle/moduleResources"
157 and: 'request data to get some modules'
158 String jsonData = TestUtils.getResourceFileContent('moduleResources.json')
159 and: 'the DMI service returns the yang resources'
160 ModuleReference moduleReference1 = new ModuleReference(name: 'ietf-yang-library', revision: '2016-06-21')
161 ModuleReference moduleReference2 = new ModuleReference(name: 'nc-notifications', revision: '2008-07-14')
162 def moduleReferences = [moduleReference1, moduleReference2]
163 def yangResources = new YangResources()
164 def yangResource = new YangResource(yangSource: '"some-data"', moduleName: 'NAME', revision: 'REVISION')
165 yangResources.add(yangResource)
166 mockDmiService.getModuleResources('some-cm-handle', moduleReferences) >> yangResources
167 when: 'the request is posted'
168 def response = mvc.perform(post(getModulesEndpoint)
169 .contentType(MediaType.APPLICATION_JSON)
170 .content(jsonData)).andReturn().response
171 then: 'a OK status is returned'
172 response.status == OK.value()
173 and: 'the response content matches the result from the DMI service'
174 response.getContentAsString() == '[{"yangSource":"\\"some-data\\"","moduleName":"NAME","revision":"REVISION"}]'
177 def 'Retrieve module resources with exception handling.'() {
178 given: 'URL to get module resources'
179 def getModulesEndpoint = "$basePathV1/ch/some-cm-handle/moduleResources"
180 and: 'request data to get some modules'
181 String jsonData = TestUtils.getResourceFileContent('moduleResources.json')
182 and: 'the system throws a not-found exception (during the processing)'
183 mockDmiService.getModuleResources('some-cm-handle', _) >> { throw Mock(ModuleResourceNotFoundException.class) }
184 when: 'the request is posted'
185 def response = mvc.perform(post(getModulesEndpoint)
186 .contentType(MediaType.APPLICATION_JSON)
187 .content(jsonData)).andReturn().response
188 then: 'a not found status is returned'
189 response.status == HttpStatus.NOT_FOUND.value()
192 def 'Get resource data for pass-through operational.'() {
193 given: 'Get resource data url and some request data'
194 def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-operational" +
195 "?resourceIdentifier=parent/child&options=(fields=myfields,depth=5)"
196 def someValidJson = '{}'
197 when: 'the request is posted'
198 def response = mvc.perform(
199 post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(someValidJson)
200 ).andReturn().response
201 then: 'response status is ok'
202 response.status == OK.value()
203 and: 'dmi service method to get resource data is invoked once'
204 1 * mockDmiService.getResourceData('some-cmHandle',
206 '(fields=myfields,depth=5)',
210 def 'Get resource data for pass-through operational with write request (invalid).'() {
211 given: 'Get resource data url'
212 def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-operational" +
213 "?resourceIdentifier=parent/child&options=(fields=myfields,depth=5)"
214 and: 'an invalid write request data for "create" operation'
215 def jsonData = '{"operation":"create"}'
216 when: 'the request is posted'
217 def response = mvc.perform(
218 post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(jsonData)
219 ).andReturn().response
220 then: 'response status is bad request'
221 response.status == BAD_REQUEST.value()
222 and: 'dmi service is not invoked'
223 0 * mockDmiService.getResourceData(*_)
226 def 'Get resource data for invalid datastore'() {
227 given: 'Get resource data url'
228 def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/dummy-datastore" +
229 "?resourceIdentifier=parent/child&options=(fields=myfields,depth=5)"
230 and: 'an invalid write request data for "create" operation'
231 def jsonData = '{"operation":"create"}'
232 when: 'the request is posted'
233 def response = mvc.perform(
234 post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(jsonData)
235 ).andReturn().response
236 then: 'response status is internal server error'
237 response.status == INTERNAL_SERVER_ERROR.value()
238 and: 'response contains expected error message'
239 response.contentAsString.contains('dummy-datastore is an invalid datastore name')
242 def 'data with #scenario operation using passthrough running.'() {
243 given: 'write data for passthrough running url'
244 def writeDataForPassthroughRunning = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-running" +
245 "?resourceIdentifier=some-resourceIdentifier"
246 and: 'request data for #scenario'
247 def jsonData = TestUtils.getResourceFileContent(requestBodyFile)
248 and: 'dmi service is called'
249 mockDmiService.writeData(operationEnum, 'some-cmHandle',
250 'some-resourceIdentifier', dataType,
251 'normal request body' ) >> '{some-json}'
252 when: 'the request is posted'
253 def response = mvc.perform(
254 post(writeDataForPassthroughRunning).contentType(MediaType.APPLICATION_JSON)
256 ).andReturn().response
257 then: 'response status is #expectedResponseStatus'
258 response.status == expectedResponseStatus
259 and: 'the response content matches the result from the DMI service'
260 response.getContentAsString() == expectedJsonResponse
261 where: 'given request body and data'
262 scenario | requestBodyFile | operationEnum | dataType || expectedResponseStatus | expectedJsonResponse
263 'Create' | 'createDataWithNormalChar.json' | CREATE | 'application/json' || CREATED.value() | '{some-json}'
264 'Update' | 'updateData.json' | UPDATE | 'application/json' || OK.value() | '{some-json}'
265 'Delete' | 'deleteData.json' | DELETE | 'application/json' || NO_CONTENT.value() | '{some-json}'
266 'Read' | 'readData.json' | READ | 'application/json' || OK.value() | ''
267 'Patch' | 'patchData.json' | PATCH | 'application/yang.patch+json' || OK.value() | '{some-json}'
270 def 'Create data using passthrough for special characters.'(){
271 given: 'create data for cmHandle url'
272 def writeDataForCmHandlePassthroughRunning = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-running" +
273 "?resourceIdentifier=some-resourceIdentifier"
274 and: 'request data with special characters'
275 def jsonData = TestUtils.getResourceFileContent('createDataWithSpecialChar.json')
276 and: 'dmi service returns data'
277 mockDmiService.writeData(CREATE, 'some-cmHandle', 'some-resourceIdentifier', 'application/json',
278 'data with quote \" and new line \n') >> '{some-json}'
279 when: 'the request is posted'
280 def response = mvc.perform(
281 post(writeDataForCmHandlePassthroughRunning).contentType(MediaType.APPLICATION_JSON).content(jsonData)
282 ).andReturn().response
283 then: 'response status is CREATED'
284 response.status == CREATED.value()
285 and: 'the response content matches the result from the DMI service'
286 response.getContentAsString() == '{some-json}'
289 def 'PassThrough Returns OK when topic is used for async'(){
290 given: 'Passthrough read URL and request data with a topic (parameter)'
291 def readPassThroughUrl ="${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:" +
293 '?resourceIdentifier=some-resourceIdentifier&topic=test-topic'
294 def jsonData = TestUtils.getResourceFileContent('readData.json')
295 when: 'the request is posted'
296 def response = mvc.perform(
297 post(readPassThroughUrl).contentType(MediaType.APPLICATION_JSON).content(jsonData)
298 ).andReturn().response
299 then: 'response status is OK'
300 assert response.status == HttpStatus.NO_CONTENT.value()
301 where: 'the following values are used'
302 resourceIdentifier << ['passthrough-operational', 'passthrough-running']
305 def 'Get resource data for pass-through running with #scenario value in resource identifier param.'() {
306 given: 'Get resource data url'
307 def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-running" +
308 "?resourceIdentifier="+resourceIdentifier+"&options=(fields=myfields,depth=5)"
309 and: 'some valid json data'
310 def json = '{"cmHandleProperties" : { "prop1" : "value1", "prop2" : "value2"}}'
311 when: 'the request is posted'
312 def response = mvc.perform(
313 post(getResourceDataForCmHandleUrl).contentType(MediaType.APPLICATION_JSON).content(json)
314 ).andReturn().response
315 then: 'response status is ok'
316 response.status == OK.value()
317 and: 'dmi service method to get resource data is invoked once with correct parameters'
318 1 * mockDmiService.getResourceData('some-cmHandle',
320 '(fields=myfields,depth=5)',
322 where: 'tokens are used in the resource identifier parameter'
323 scenario | resourceIdentifier
324 '/' | 'id/with/slashes'
329 '? needs to be encoded as %3F' | 'idWith%3F'
333 def 'Execute a data operation for a list of operations.'() {
334 given: 'an endpoint for a data operation request with list of cmhandles in request body'
335 def resourceDataUrl = "$basePathV1/data?topic=client-topic-name&requestId=some-requestId"
336 and: 'list of operation details are received into request body'
337 def dataOperationRequestBody = '[{"operation": "read", "operationId": "14", "datastore": "ncmp-datastore:passthrough-operational", "options": "some options", "resourceIdentifier": "some resourceIdentifier",' +
338 ' "cmhandles": [ {"id": "cmHanlde123", "cmHandleProperties": { "myProp`": "some value", "otherProp": "other value"}}]}]'
339 when: 'the dmi resource data for dataOperation api is called.'
340 def response = mvc.perform(
341 post(resourceDataUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestBody)
342 ).andReturn().response
343 then: 'the resource data operation endpoint returns the not implemented response'
344 assert response.status == 501