Async request response NCMP -> Client
[cps.git] / cps-ncmp-rest / src / test / groovy / org / onap / cps / ncmp / rest / controller / NetworkCmProxyControllerSpec.groovy
1 /*
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
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
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.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.ncmp.rest.controller
25
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.models.NcmpServiceCmHandle
30 import org.onap.cps.ncmp.rest.mapper.RestOutputCmHandleStateMapper
31 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
32 import spock.lang.Shared
33
34 import java.time.OffsetDateTime
35 import java.time.ZoneOffset
36 import java.time.format.DateTimeFormatter
37
38 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.PATCH
39 import static org.onap.cps.ncmp.api.inventory.CompositeState.DataStores
40 import static org.onap.cps.ncmp.api.inventory.CompositeState.LockReason
41 import static org.onap.cps.ncmp.api.inventory.CompositeState.Operational
42 import static org.onap.cps.ncmp.api.inventory.CompositeState.Running
43 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
44 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
45 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
46 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
47 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
48 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.CREATE
49 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.UPDATE
50 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.DELETE
51
52 import com.fasterxml.jackson.databind.ObjectMapper
53 import org.onap.cps.TestUtils
54 import org.onap.cps.spi.model.ModuleReference
55 import org.onap.cps.utils.JsonObjectMapper
56 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
57 import org.spockframework.spring.SpringBean
58 import org.springframework.beans.factory.annotation.Autowired
59 import org.springframework.beans.factory.annotation.Value
60 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
61 import org.springframework.http.HttpStatus
62 import org.springframework.http.MediaType
63 import org.springframework.test.web.servlet.MockMvc
64 import spock.lang.Specification
65
66 @WebMvcTest(NetworkCmProxyController)
67 class NetworkCmProxyControllerSpec extends Specification {
68
69     @Autowired
70     MockMvc mvc
71
72     @SpringBean
73     NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
74
75     @SpringBean
76     ObjectMapper objectMapper = new ObjectMapper()
77
78     @SpringBean
79     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
80
81     @SpringBean
82     NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
83
84     @SpringBean
85     RestOutputCmHandleStateMapper restOutputCmHandleStateMapper = Mappers.getMapper(RestOutputCmHandleStateMapper)
86
87     @SpringBean
88     CpsNcmpTaskExecutor spiedCpsTaskExecutor = Spy()
89
90     @Value('${rest.api.ncmp-base-path}/v1')
91     def ncmpBasePathV1
92
93     def requestBody = '{"some-key":"some-value"}'
94
95     @Shared
96     def NO_TOPIC = null
97     def NO_REQUEST_ID = null
98
99     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
100         .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
101
102     def 'Get Resource Data from pass-through operational.'() {
103         given: 'resource data url'
104             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
105                     "?resourceIdentifier=parent/child&options=(a=1,b=2)"
106         when: 'get data resource request is performed'
107             def response = mvc.perform(
108                     get(getUrl)
109                             .contentType(MediaType.APPLICATION_JSON)
110             ).andReturn().response
111         then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle'
112             1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
113                     'parent/child',
114                     '(a=1,b=2)',
115                     NO_TOPIC,
116                     NO_REQUEST_ID)
117         and: 'response status is Ok'
118             response.status == HttpStatus.OK.value()
119     }
120
121     def 'Get Resource Data from #datastoreInUrl with #scenario.'() {
122         given: 'resource data url'
123             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
124                     "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
125         when: 'get data resource request is performed'
126             def response = mvc.perform(
127                     get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
128         then: 'task executor is called appropriate number of times'
129             expectedNumberOfExecutorExecutions * spiedCpsTaskExecutor.executeTask(_, 2000)
130         and: 'response status is expected'
131             response.status == HttpStatus.OK.value()
132         where: 'the following parameters are used'
133             scenario                               | datastoreInUrl            | topicQueryParam        || expectedTopicName | expectedNumberOfExecutorExecutions
134             'url with valid topic'                 | 'passthrough-operational' | '&topic=my-topic-name' || 'my-topic-name'   | 1
135             'no topic in url'                      | 'passthrough-operational' | ''                     || NO_TOPIC          | 0
136             'null topic in url'                    | 'passthrough-operational' | '&topic=null'          || 'null'            | 1
137             'url with valid topic'                 | 'passthrough-running'     | '&topic=my-topic-name' || 'my-topic-name'   | 1
138             'no topic in url'                      | 'passthrough-running'     | ''                     || NO_TOPIC          | 0
139             'null topic in url'                    | 'passthrough-running'     | '&topic=null'          || 'null'            | 1
140     }
141
142     def 'Fail to get Resource Data from #datastoreInUrl when #scenario.'() {
143         given: 'resource data url'
144             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
145                 "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
146         when: 'get data resource request is performed'
147             def response = mvc.perform(
148                 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
149         then: 'abad request is returned'
150             response.status == HttpStatus.BAD_REQUEST.value()
151         where: 'the following parameters are used'
152             scenario                               | datastoreInUrl            | topicQueryParam
153             'empty topic in url'                   | 'passthrough-operational' | '&topic=\"\"'
154             'missing topic in url'                 | 'passthrough-operational' | '&topic='
155             'blank topic value in url'             | 'passthrough-operational' | '&topic=\" \"'
156             'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#'
157             'empty topic in url'                   | 'passthrough-running'     | '&topic=\"\"'
158             'missing topic in url'                 | 'passthrough-running'     | '&topic='
159             'blank topic value in url'             | 'passthrough-running'     | '&topic=\" \"'
160             'invalid non-empty topic value in url' | 'passthrough-running'     | '&topic=1_5_*_#'
161     }
162
163     def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
164         given: 'resource data url'
165             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
166                     "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)"
167         and: 'ncmp service returns json object'
168             mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
169                     resourceIdentifier,
170                     '(a=1,b=2)',
171                     NO_TOPIC,
172                     NO_REQUEST_ID) >> '{valid-json}'
173         when: 'get data resource request is performed'
174             def response = mvc.perform(
175                     get(getUrl)
176                             .contentType(MediaType.APPLICATION_JSON)
177             ).andReturn().response
178         then: 'response status is Ok'
179             response.status == HttpStatus.OK.value()
180         and: 'response contains valid object body'
181             response.getContentAsString() == '{valid-json}'
182         where: 'tokens are used in the resource identifier parameter'
183             scenario                       | resourceIdentifier
184             '/'                            | 'id/with/slashes'
185             '?'                            | 'idWith?'
186             ','                            | 'idWith,'
187             '='                            | 'idWith='
188             '[]'                           | 'idWith[]'
189             '? needs to be encoded as %3F' | 'idWith%3F'
190     }
191
192     def 'Update resource data from pass-through running.' () {
193         given: 'update resource data url'
194             def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
195                 "?resourceIdentifier=parent/child"
196         when: 'update data resource request is performed'
197             def response = mvc.perform(
198                 put(updateUrl)
199                     .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
200             ).andReturn().response
201         then: 'ncmp service method to update resource is called'
202             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
203                 'parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8')
204         and: 'the response status is OK'
205             response.status == HttpStatus.OK.value()
206     }
207
208     def 'Create Resource Data from pass-through running with #scenario.' () {
209         given: 'resource data url'
210             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
211                     "?resourceIdentifier=parent/child"
212             def requestBody = '{"some-key":"some-value"}'
213         when: 'create resource request is performed'
214             def response = mvc.perform(
215                     post(url)
216                             .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
217             ).andReturn().response
218         then: 'ncmp service method to create resource called'
219             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
220                 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8')
221         and: 'resource is created'
222             response.status == HttpStatus.CREATED.value()
223     }
224
225     def 'Get module references for the given dataspace and cm handle.' () {
226         given: 'get module references url'
227             def getUrl = "$ncmpBasePathV1/ch/some-cmhandle/modules"
228         when: 'get module resource request is performed'
229             def response =mvc.perform(get(getUrl)).andReturn().response
230         then: 'ncmp service method to get yang resource module references is called'
231             mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle')
232                     >> [new ModuleReference(moduleName: 'some-name1',revision: '2021-10-03')]
233         and: 'response contains an array with the module name and revision'
234             response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]'
235         and: 'response returns an OK http code'
236             response.status == HttpStatus.OK.value()
237     }
238
239     def 'Retrieve cm handles.'() {
240         given: 'an endpoint and json data'
241             def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
242             String jsonString = TestUtils.getResourceFileContent('cmhandle-search.json')
243         and: 'the service method is invoked with module names and returns two cm handle ids'
244             mockNetworkCmProxyDataService.executeCmHandleHasAllModulesSearch(['module1', 'module2']) >> ['some-cmhandle-id1', 'some-cmhandle-id2']
245         when: 'the searches api is invoked'
246             def response = mvc.perform(post(searchesEndpoint)
247                     .contentType(MediaType.APPLICATION_JSON)
248                     .content(jsonString)).andReturn().response
249         then: 'response status returns OK'
250             response.status == HttpStatus.OK.value()
251         and: 'the expected response content is returned'
252             response.contentAsString == '{"cmHandles":[{"cmHandleId":"some-cmhandle-id1"},{"cmHandleId":"some-cmhandle-id2"}]}'
253     }
254
255     def 'Get Cm Handle details by Cm Handle id.'() {
256         given: 'an endpoint and a cm handle'
257             def cmHandleDetailsEndpoint = "$ncmpBasePathV1/ch/some-cm-handle"
258         and: 'an existing ncmp service cm handle'
259             def cmHandleId = 'some-cm-handle'
260             def dmiProperties = [ prop:'some DMI property' ]
261             def publicProperties = [ "public prop":'some public property' ]
262             def compositeState = new CompositeState(cmhandleState: CmHandleState.ADVISED,
263                 lockReason: LockReason.builder().reason('LOCKED_OTHER').details("lock-misbehaving-details").build(),
264                 lastUpdateTime: formattedDateAndTime.toString(),
265                 dataSyncEnabled: false,
266                 dataStores: dataStores())
267             def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
268         and: 'the service method is invoked with the cm handle id'
269             1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('some-cm-handle') >> ncmpServiceCmHandle
270         when: 'the cm handle details api is invoked'
271             def response = mvc.perform(get(cmHandleDetailsEndpoint)).andReturn().response
272         then: 'the correct response is returned'
273             response.status == HttpStatus.OK.value()
274         and: 'the response returns public properties and the correct cm handle states'
275             response.contentAsString.contains('publicCmHandleProperties')
276             response.contentAsString.contains('LOCKED_OTHER')
277             response.contentAsString.contains('lock-misbehaving-details')
278             response.contentAsString.contains('ADVISED')
279             response.contentAsString.contains('NONE_REQUESTED')
280             response.contentAsString.contains('2022-12-31T20:30:40.000+0000')
281         and: 'the content does not contain dmi properties'
282             !response.contentAsString.contains("some DMI property")
283     }
284
285     def 'Get Cm Handle public properties by Cm Handle id.' () {
286         given: 'a cm handle properties endpoint'
287             def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/properties"
288         and: 'some cm handle public properties'
289             def publicProperties =  [ 'public prop':'some public property' ]
290         and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
291             1 * mockNetworkCmProxyDataService.getCmHandlePublicProperties('some-cm-handle') >> publicProperties
292         when: 'the cm handle properties api is invoked'
293             def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response
294         then: 'the correct response is returned'
295             response.status == HttpStatus.OK.value()
296         and: 'the response returns public properties and the correct properties'
297             response.contentAsString.equals('{"publicCmHandleProperties":[{"public prop":"some public property"}]}')
298     }
299
300     def 'Call execute cm handle searches with unrecognized condition name.'() {
301         given: 'an endpoint and json data'
302             def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
303             String jsonString = TestUtils.getResourceFileContent('invalid-cmhandle-search.json')
304         when: 'the searches api is invoked'
305             def response = mvc.perform(post(searchesEndpoint)
306                     .contentType(MediaType.APPLICATION_JSON)
307                     .content(jsonString)).andReturn().response
308         then: 'an empty cm handle identifier is returned'
309             response.contentAsString == '{"cmHandles":[]}'
310     }
311
312     def 'Query for cm handles matching query parameters'() {
313         given: 'an endpoint and json data'
314             def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches"
315             String jsonString = '{"publicCmHandleProperties": {"name": "Contact", "value": "newemailforstore@bookstore.com"}}'
316         and: 'the service method is invoked with module names and returns cm handle ids'
317             1 * mockNetworkCmProxyDataService.queryCmHandles(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2']
318         when: 'the searches api is invoked'
319             def response = mvc.perform(post(searchesEndpoint)
320                 .contentType(MediaType.APPLICATION_JSON)
321                 .content(jsonString)).andReturn().response
322         then: 'cm handle ids are returned'
323             response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]'
324     }
325
326     def 'Query for cm handles with invalid request payload'() {
327         when: 'the searches api is invoked'
328             def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches"
329             def invalidInputData = '{invalidJson}'
330             def response = mvc.perform(post(searchesEndpoint)
331                     .contentType(MediaType.APPLICATION_JSON)
332                     .content(invalidInputData)).andReturn().response
333         then: 'BAD_REQUEST is returned'
334             response.getStatus() == 400
335     }
336
337     def 'Patch resource data in pass-through running datastore.' () {
338         given: 'patch resource data url'
339             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
340                     "?resourceIdentifier=parent/child"
341         when: 'patch data resource request is performed'
342             def response = mvc.perform(
343                     patch(url)
344                             .contentType(MediaType.APPLICATION_JSON)
345                             .accept(MediaType.APPLICATION_JSON).content(requestBody)
346             ).andReturn().response
347         then: 'ncmp service method to update resource is called'
348             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
349                     'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8')
350         and: 'the response status is OK'
351             response.status == HttpStatus.OK.value()
352     }
353
354     def 'Delete resource data in pass-through running datastore.' () {
355         given: 'delete resource data url'
356             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
357                      "?resourceIdentifier=parent/child"
358         when: 'delete data resource request is performed'
359             def response = mvc.perform(
360                 delete(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
361         then: 'the ncmp service method to delete resource is called (with null as body)'
362             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
363                 'parent/child', DELETE, null, 'application/json;charset=UTF-8')
364         and: 'the response is No Content'
365             response.status == HttpStatus.NO_CONTENT.value()
366     }
367
368     def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
369         given: 'resource data url'
370             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
371                     "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
372         when: 'get data resource request is performed'
373             def response = mvc.perform(
374                     get(getUrl)
375                             .contentType(MediaType.APPLICATION_JSON)
376                             .accept(MediaType.APPLICATION_JSON_VALUE)
377             ).andReturn().response
378         then: 'async request id is generated'
379             assert response.contentAsString.contains("requestId")
380         where: 'the following parameters are used'
381             scenario                   | datastoreInUrl
382             ':passthrough-operational' | 'passthrough-operational'
383             ':passthrough-running'     | 'passthrough-running'
384     }
385
386     def dataStores() {
387         DataStores.builder()
388             .operationalDataStore(Operational.builder()
389                 .syncState('NONE_REQUESTED')
390                 .lastSyncTime(formattedDateAndTime.toString()).build())
391             .runningDataStore(Running.builder()
392                 .syncState('NONE_REQUESTED')
393                 .lastSyncTime(formattedDateAndTime.toString()).build())
394             .build()
395     }
396
397 }
398