fb411c054a026aa318b4f0c69cece5604808b182
[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-2023 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 com.fasterxml.jackson.databind.ObjectMapper
27 import org.mapstruct.factory.Mappers
28 import org.onap.cps.TestUtils
29 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
30 import org.onap.cps.ncmp.api.NetworkCmProxyQueryService
31 import org.onap.cps.ncmp.api.inventory.CmHandleState
32 import org.onap.cps.ncmp.api.inventory.CompositeState
33 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
34 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
35 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
36 import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler
37 import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler
38 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
39 import org.onap.cps.ncmp.rest.mapper.CmHandleStateMapper
40 import org.onap.cps.ncmp.rest.util.DeprecationHelper
41 import org.onap.cps.spi.FetchDescendantsOption
42 import org.onap.cps.spi.model.ModuleDefinition
43 import org.onap.cps.spi.model.ModuleReference
44 import org.onap.cps.utils.JsonObjectMapper
45 import org.spockframework.spring.SpringBean
46 import org.springframework.beans.factory.annotation.Autowired
47 import org.springframework.beans.factory.annotation.Value
48 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
49 import org.springframework.http.HttpStatus
50 import org.springframework.http.MediaType
51 import org.springframework.test.web.servlet.MockMvc
52 import spock.lang.Shared
53 import spock.lang.Specification
54 import java.time.OffsetDateTime
55 import java.time.ZoneOffset
56 import java.time.format.DateTimeFormatter
57
58 import static org.onap.cps.ncmp.api.inventory.CompositeState.DataStores
59 import static org.onap.cps.ncmp.api.inventory.CompositeState.Operational
60 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
61 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
62 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
63 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
64 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
65 import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.CREATE
66 import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.UPDATE
67 import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.PATCH
68 import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.DELETE
69 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
70 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
71 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
72 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
73 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
74
75
76 @WebMvcTest(NetworkCmProxyController)
77 class NetworkCmProxyControllerSpec extends Specification {
78
79     @Autowired
80     MockMvc mvc
81
82     @SpringBean
83     NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
84
85     @SpringBean
86     NetworkCmProxyQueryService mockNetworkCmProxyQueryService = Mock()
87
88     @SpringBean
89     ObjectMapper objectMapper = new ObjectMapper()
90
91     @SpringBean
92     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
93
94     @SpringBean
95     NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
96
97     @SpringBean
98     CmHandleStateMapper cmHandleStateMapper = Mappers.getMapper(CmHandleStateMapper)
99
100     @SpringBean
101     CpsNcmpTaskExecutor spiedCpsTaskExecutor = Spy()
102
103     @SpringBean
104     DeprecationHelper stubbedDeprecationHelper = Stub()
105
106     @SpringBean
107     NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler = new NcmpCachedResourceRequestHandler(spiedCpsTaskExecutor, mockNetworkCmProxyDataService, mockNetworkCmProxyQueryService)
108
109     @SpringBean
110     NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler = new NcmpPassthroughResourceRequestHandler(spiedCpsTaskExecutor, mockNetworkCmProxyDataService)
111
112     @Value('${rest.api.ncmp-base-path}/v1')
113     def ncmpBasePathV1
114
115     def requestBody = '{"some-key":"some-value"}'
116     def bulkRequestBody = '["testCmHandle"]'
117
118     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
119
120     @Shared
121     def NO_TOPIC = null
122     def NO_REQUEST_ID = null
123     def TIMOUT_FOR_TEST = 1234
124
125     def setup() {
126         ncmpCachedResourceRequestHandler.notificationFeatureEnabled = true
127         ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
128         ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = true
129         ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
130     }
131
132     def 'Get Resource Data from pass-through operational.'() {
133         given: 'resource data url'
134             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
135                 "?resourceIdentifier=parent/child&options=(a=1,b=2)"
136         when: 'get data resource request is performed'
137             def response = mvc.perform(
138                 get(getUrl)
139                     .contentType(MediaType.APPLICATION_JSON)
140             ).andReturn().response
141         then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle'
142             1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle',
143                 'parent/child','(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID)
144         and: 'response status is Ok'
145             response.status == HttpStatus.OK.value()
146     }
147
148     def 'Get Resource Data Async Topic Handling with #scenario.'() {
149         given: 'resource data url'
150             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational?resourceIdentifier=parent/child&${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: 'task executor is called appropriate number of times'
155             expectedNumberOfTaskExecutions * spiedCpsTaskExecutor.executeTask(_, TIMOUT_FOR_TEST)
156         and: 'response status is OK'
157             response.status == HttpStatus.OK.value()
158         where: 'the following parameters are used'
159             scenario               | datastoreInUrl            | topicQueryParam        || expectedNumberOfTaskExecutions
160             'url with valid topic' | 'passthrough-operational' | '&topic=my-topic-name' || 1
161             'no topic in url'      | 'passthrough-operational' | ''                     || 0
162             'null topic in url'    | 'passthrough-operational' | '&topic=null'          || 1
163     }
164
165     def 'Get Resource Data from ncmp-datastore:operational (cached) parameters handling with #scenario.'() {
166         given: 'resource data url'
167             def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational" +
168                 "?resourceIdentifier=parent/child${additionalUrlParam}"
169         when: 'get data resource request is performed'
170             def response = mvc.perform(
171                 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
172         then: 'task executor is called appropriate number of times'
173             1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle('ncmp-datastore:operational', 'h123', 'parent/child', expectedIncludeDescendants)
174         and: 'response status is OK'
175             response.status == HttpStatus.OK.value()
176         where: 'the following parameters are used'
177             scenario                    | additionalUrlParam           || expectedIncludeDescendants
178             'no additional param'       | ''                           || OMIT_DESCENDANTS
179             'include descendants true'  | '&include-descendants=true'  || INCLUDE_ALL_DESCENDANTS
180             'include descendants TRUE'  | '&include-descendants=true'  || INCLUDE_ALL_DESCENDANTS
181             'include descendants false' | '&include-descendants=false' || OMIT_DESCENDANTS
182             'include descendants FALSE' | '&include-descendants=FALSE' || OMIT_DESCENDANTS
183             'options (ignored)'         | '&options=(a-=1)'            || OMIT_DESCENDANTS
184     }
185
186     def 'Get Resource Data with invalid topic parameter: #scenario.'() {
187         given: 'resource data url'
188             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
189                 "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
190         when: 'get data resource (async) request is performed'
191             def response = mvc.perform(
192                 get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
193         then: 'abad request is returned'
194             response.status == HttpStatus.BAD_REQUEST.value()
195         where: 'the following parameters are used'
196             scenario                               | datastoreInUrl            | topicQueryParam
197             'empty topic in url'                   | 'passthrough-operational' | '&topic=\"\"'
198             'missing topic in url'                 | 'passthrough-operational' | '&topic='
199             'blank topic value in url'             | 'passthrough-operational' | '&topic=\" \"'
200             'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#'
201     }
202
203     def 'Get (async) bulk resource data from dmi service.'() {
204         given: 'bulk resource data url'
205             def getUrl = "$ncmpBasePathV1/batch/data/ds/${datastore.datastoreName}" +
206                     "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=myTopic"
207         when: 'post data resource request is performed'
208             def response = mvc.perform(
209                     post(getUrl)
210                             .contentType(MediaType.APPLICATION_JSON)
211                             .content(bulkRequestBody)
212             ).andReturn().response
213         then: 'response status is Ok'
214             response.status == HttpStatus.OK.value()
215         and: 'async request id is generated'
216             assert response.contentAsString.contains("requestId")
217         then: 'wait a little to allow execution of service method by task executor (on separate thread)'
218             Thread.sleep(100);
219         then: 'the service has been invoked with the correct parameters '
220             1 * mockNetworkCmProxyDataService.getResourceDataForCmHandleBatch(datastore.datastoreName, ['testCmHandle'],
221                     'parent/child',
222                    '(a=1,b=2)',
223                   'myTopic',
224                  _)
225         where: 'the following data stores are used'
226             datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
227     }
228
229     def 'Get bulk resource data for non-supported #datastoreName from dmi service.'() {
230         given: 'bulk resource data url'
231             def getUrl = "$ncmpBasePathV1/batch/data/ds/ncmp-datastore:operational" +
232                     "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=myTopic"
233         when: 'post data resource request is performed'
234             def response = mvc.perform(
235                     post(getUrl)
236                             .contentType(MediaType.APPLICATION_JSON)
237                             .content(bulkRequestBody)
238             ).andReturn().response
239         then: 'response status code is 501 not implemented'
240             response.status == HttpStatus.NOT_IMPLEMENTED.value()
241     }
242
243     def 'Query Resource Data from operational.'() {
244         given: 'the query resource data url'
245             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query" +
246                 "?cps-path=/cps/path"
247         when: 'the query data resource request is performed'
248             def response = mvc.perform(
249                 get(getUrl)
250                     .contentType(MediaType.APPLICATION_JSON)
251             ).andReturn().response
252         then: 'the NCMP query service is called with queryResourceDataOperationalForCmHandle'
253             1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle',
254                 '/cps/path',
255                 FetchDescendantsOption.OMIT_DESCENDANTS)
256         and: 'response status is Ok'
257             response.status == HttpStatus.OK.value()
258     }
259
260     def 'Query Resource Data using datastore of #datastore'() {
261         given: 'the query resource data url'
262             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastore}/query" +
263                 "?cps-path=/cps/path"
264         when: 'the query data resource request is performed'
265             def response = mvc.perform(
266                 get(getUrl)
267                     .contentType(MediaType.APPLICATION_JSON)
268             ).andReturn().response
269         then: 'a 400 BAD_REQUEST is returned for the unsupported datastore'
270             response.status == 400
271         and: 'the error message is that datastore #datastore is not supported'
272             response.contentAsString.contains("ncmp-datastore:${datastore} is not supported")
273         where: 'the following datastore is used'
274             datastore << ["passthrough-running", "passthrough-operational"]
275     }
276
277     def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
278         given: 'resource data url'
279             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
280                 "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)"
281         and: 'ncmp service returns json object'
282             mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle',
283                 resourceIdentifier,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID) >> '{valid-json}'
284         when: 'get data resource request is performed'
285             def response = mvc.perform(
286                 get(getUrl)
287                     .contentType(MediaType.APPLICATION_JSON)
288             ).andReturn().response
289         then: 'response status is Ok'
290             response.status == HttpStatus.OK.value()
291         and: 'response contains valid object body'
292             response.getContentAsString() == '{valid-json}'
293         where: 'tokens are used in the resource identifier parameter'
294             scenario                       | resourceIdentifier
295             '/'                            | 'id/with/slashes'
296             '?'                            | 'idWith?'
297             ','                            | 'idWith,'
298             '='                            | 'idWith='
299             '[]'                           | 'idWith[]'
300             '? needs to be encoded as %3F' | 'idWith%3F'
301     }
302
303     def 'Update resource data from pass-through running.'() {
304         given: 'update resource data url'
305             def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
306                 "?resourceIdentifier=parent/child"
307         when: 'update data resource request is performed'
308             def response = mvc.perform(
309                 put(updateUrl)
310                     .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
311             ).andReturn().response
312         then: 'ncmp service method to update resource is called'
313             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
314                 'parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8')
315         and: 'the response status is OK'
316             response.status == HttpStatus.OK.value()
317     }
318
319     def 'Create Resource Data from pass-through running with #scenario.'() {
320         given: 'resource data url'
321             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
322                 "?resourceIdentifier=parent/child"
323         when: 'create resource request is performed'
324             def response = mvc.perform(
325                 post(url)
326                     .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
327             ).andReturn().response
328         then: 'ncmp service method to create resource called'
329             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
330                 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8')
331         and: 'resource is created'
332             response.status == HttpStatus.CREATED.value()
333     }
334
335     def 'Get module references for the given dataspace and cm handle.'() {
336         given: 'get module references url'
337             def getUrl = "$ncmpBasePathV1/ch/some-cmhandle/modules"
338         when: 'get module resource request is performed'
339             def response = mvc.perform(get(getUrl)).andReturn().response
340         then: 'ncmp service method to get yang resource module references is called'
341             mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle')
342                 >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')]
343         and: 'response contains an array with the module name and revision'
344             response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]'
345         and: 'response returns an OK http code'
346             response.status == HttpStatus.OK.value()
347     }
348
349     def 'Retrieve cm handles.'() {
350         given: 'an endpoint and json data'
351             def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
352             String jsonString = TestUtils.getResourceFileContent('cmhandle-search.json')
353         and: 'the service method is invoked with module names and returns two cm handles'
354             def cmHandle1 = new NcmpServiceCmHandle()
355             cmHandle1.cmHandleId = 'some-cmhandle-id1'
356             cmHandle1.publicProperties = [color: 'yellow']
357             def cmHandle2 = new NcmpServiceCmHandle()
358             cmHandle2.cmHandleId = 'some-cmhandle-id2'
359             cmHandle2.publicProperties = [color: 'green']
360             mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandle1, cmHandle2]
361         when: 'the searches api is invoked'
362             def response = mvc.perform(post(searchesEndpoint)
363                 .contentType(MediaType.APPLICATION_JSON)
364                 .content(jsonString)).andReturn().response
365         then: 'response status returns OK'
366             response.status == HttpStatus.OK.value()
367         and: 'the expected response content is returned'
368             response.contentAsString == '[{"cmHandle":"some-cmhandle-id1","publicCmHandleProperties":[{"color":"yellow"}],"state":null},{"cmHandle":"some-cmhandle-id2","publicCmHandleProperties":[{"color":"green"}],"state":null}]'
369     }
370
371     def 'Get complete Cm Handle details by Cm Handle id.'() {
372         given: 'an endpoint and a cm handle'
373             def cmHandleDetailsEndpoint = "$ncmpBasePathV1/ch/some-cm-handle"
374         and: 'an existing ncmp service cm handle'
375             def cmHandleId = 'some-cm-handle'
376             def dmiProperties = [prop: 'some DMI property']
377             def publicProperties = ["public prop": 'some public property']
378             def compositeState = compositeStateTestObject()
379             def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
380         and: 'the service method is invoked with the cm handle id'
381             1 * mockNetworkCmProxyDataService.getNcmpServiceCmHandle('some-cm-handle') >> ncmpServiceCmHandle
382         when: 'the cm handle details api is invoked'
383             def response = mvc.perform(
384                 get(cmHandleDetailsEndpoint)).andReturn().response
385         then: 'the correct response is returned'
386             response.status == HttpStatus.OK.value()
387         and: 'the response contains the public properties'
388             assertContainsPublicProperties(response)
389         and: 'the response contains the cm handle state'
390             assertContainsState(response)
391         and: 'the content does not contain dmi properties'
392             !response.contentAsString.contains("some DMI property")
393     }
394
395     def 'Get Cm Handle public properties by Cm Handle id.'() {
396         given: 'a cm handle properties endpoint'
397             def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/properties"
398         and: 'some cm handle public properties'
399             def publicProperties = ['public prop': 'some public property']
400         and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
401             1 * mockNetworkCmProxyDataService
402                 .getCmHandlePublicProperties('some-cm-handle') >> publicProperties
403         when: 'the cm handle properties api is invoked'
404             def response = mvc.perform(
405                 get(cmHandlePropertiesEndpoint)).andReturn().response
406         then: 'the correct response is returned'
407             response.status == HttpStatus.OK.value()
408         and: 'the response contains the public properties'
409             assertContainsPublicProperties(response)
410     }
411
412     def 'Get Cm Handle composite state by Cm Handle id.'() {
413         given: 'a cm handle state endpoint'
414             def cmHandlePropertiesEndpoint = "$ncmpBasePathV1/ch/some-cm-handle/state"
415         and: 'some cm handle composite state'
416             def compositeState = compositeStateTestObject()
417         and: 'the service method is invoked with the cm handle id returning the cm handle composite state'
418             1 * mockNetworkCmProxyDataService
419                 .getCmHandleCompositeState('some-cm-handle') >> compositeState
420         when: 'the cm handle state api is invoked'
421             def response = mvc.perform(
422                 get(cmHandlePropertiesEndpoint)).andReturn().response
423         then: 'the correct response is returned'
424             response.status == HttpStatus.OK.value()
425         and: 'the response contains the cm handle state'
426             assertContainsState(response)
427     }
428
429     def 'Call execute cm handle searches with unrecognized condition name.'() {
430         given: 'an endpoint and json data'
431             def searchesEndpoint = "$ncmpBasePathV1/ch/searches"
432             String jsonString = TestUtils.getResourceFileContent('invalid-cmhandle-search.json')
433         and: 'the service method is invoked with module names and returns two cm handles'
434             def cmHandel1 = new NcmpServiceCmHandle()
435             cmHandel1.cmHandleId = 'some-cmhandle-id1'
436             cmHandel1.publicProperties = [color: 'yellow']
437             def cmHandel2 = new NcmpServiceCmHandle()
438             cmHandel2.cmHandleId = 'some-cmhandle-id2'
439             cmHandel2.publicProperties = [color: 'green']
440             mockNetworkCmProxyDataService.executeCmHandleSearch(_) >> [cmHandel1, cmHandel2]
441         when: 'the searches api is invoked'
442             def response = mvc.perform(
443                 post(searchesEndpoint)
444                     .contentType(MediaType.APPLICATION_JSON)
445                     .content(jsonString)).andReturn().response
446         then: 'an empty cm handle identifier is returned'
447             response.contentAsString == '[{"cmHandle":"some-cmhandle-id1","publicCmHandleProperties":[{"color":"yellow"}],"state":null},{"cmHandle":"some-cmhandle-id2","publicCmHandleProperties":[{"color":"green"}],"state":null}]'
448     }
449
450     def 'Query for cm handles matching query parameters'() {
451         given: 'an endpoint and json data'
452             def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
453         and: 'the service method is invoked with module names and returns cm handle ids'
454             1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2']
455         when: 'the searches api is invoked'
456             def response = mvc.perform(
457                 post(searchesEndpoint)
458                     .contentType(MediaType.APPLICATION_JSON)
459                     .content('{}')).andReturn().response
460         then: 'cm handle ids are returned'
461             response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]'
462     }
463
464     def 'Query for cm handles with invalid request payload'() {
465         when: 'the searches api is invoked'
466             def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
467             def invalidInputData = '{invalidJson}'
468             def response = mvc.perform(
469                 post(searchesEndpoint)
470                     .contentType(MediaType.APPLICATION_JSON)
471                     .content(invalidInputData)).andReturn().response
472         then: 'BAD_REQUEST is returned'
473             response.getStatus() == 400
474     }
475
476     def 'Patch resource data in pass-through running datastore.'() {
477         given: 'patch resource data url'
478             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
479                 "?resourceIdentifier=parent/child"
480         when: 'patch data resource request is performed'
481             def response = mvc.perform(
482                 patch(url)
483                     .contentType(MediaType.APPLICATION_JSON)
484                     .accept(MediaType.APPLICATION_JSON).content(requestBody)
485             ).andReturn().response
486         then: 'ncmp service method to update resource is called'
487             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
488                 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8')
489         and: 'the response status is OK'
490             response.status == HttpStatus.OK.value()
491     }
492
493     def 'Delete resource data in pass-through running datastore.'() {
494         given: 'delete resource data url'
495             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
496                 "?resourceIdentifier=parent/child"
497         when: 'delete data resource request is performed'
498             def response = mvc.perform(
499                 delete(url)
500                     .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
501         then: 'the ncmp service method to delete resource is called (with null as body)'
502             1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
503                 'parent/child', DELETE, null, 'application/json;charset=UTF-8')
504         and: 'the response is No Content'
505             response.status == HttpStatus.NO_CONTENT.value()
506     }
507
508     def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
509         given: 'resource data url'
510             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
511                 "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
512         when: 'get data resource request is performed'
513             def response = mvc.perform(
514                 get(getUrl)
515                     .contentType(MediaType.APPLICATION_JSON)
516                     .accept(MediaType.APPLICATION_JSON_VALUE)
517             ).andReturn().response
518         then: 'async request id is generated'
519             assert response.contentAsString.contains("requestId")
520         where: 'the following parameters are used'
521             scenario                   | datastoreInUrl
522             ':passthrough-operational' | 'passthrough-operational'
523             ':passthrough-running'     | 'passthrough-running'
524     }
525
526     def 'Get module definitions based on cmHandleId.'() {
527         when: 'get module definition request is performed'
528             def response = mvc.perform(
529                 get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions"))
530                 .andReturn().response
531         then: 'ncmp service method to get module definitions is called'
532             mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleId('some-cmhandle')
533                 >> [new ModuleDefinition('sampleModuleName', '2021-10-03',
534                 'module sampleModuleName{ sample module content }')]
535         and: 'response contains an array with the module name, revision and content'
536             response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]'
537         and: 'response returns an OK http code'
538             response.status == HttpStatus.OK.value()
539     }
540
541     def 'Set the data sync enabled based on the cm handle id and the data sync flag is #scenario'() {
542         when: 'the set data sync enabled request is invoked'
543             def response = mvc.perform(
544                 put("$ncmpBasePathV1/ch/some-cm-handle-id/data-sync?dataSyncEnabled=" + dataSyncEnabledFlag))
545                 .andReturn().response
546         then: 'method to set data sync enabled is called'
547             1 * mockNetworkCmProxyDataService.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
548         and: 'the response returns an OK http code'
549             response.status == HttpStatus.OK.value()
550         where: 'the following parameters are used'
551             scenario   | dataSyncEnabledFlag
552             'enabled'  | true
553             'disabled' | false
554     }
555
556     def 'Get Resource Data from operational with or without descendants'() {
557         given: 'resource data url with descendants #enabled'
558             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational" +
559                 "?resourceIdentifier=parent/child&include-descendants=${enabled}"
560         when: 'get data resource request is performed'
561             def response = mvc.perform(
562                 get(getUrl)
563                     .contentType(MediaType.APPLICATION_JSON)
564             ).andReturn().response
565         then: 'the NCMP data service is called with getResourceDataOperational with #descendantsOption'
566             1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child', descendantsOption)
567         and: 'response status is Ok'
568             response.status == HttpStatus.OK.value()
569         where: 'the following parameters are used'
570             enabled | descendantsOption
571             false   | FetchDescendantsOption.OMIT_DESCENDANTS
572             true    | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
573     }
574
575     def 'Attempt execute #operation rest operation on resource data with #scenario'() {
576         given: 'resource data url'
577             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/${datastoreInUrl}?resourceIdentifier=parent/child"
578         when: 'selected request for data resource is performed on url'
579             def response = mvc.perform(
580                 executeRestOperation(operation, url))
581                 .andReturn().response
582         then: 'the response status is as expected'
583             assert response.status == HttpStatus.BAD_REQUEST.value()
584         and: 'the response is as expected'
585             assert response.getContentAsString().contains(datastoreInUrl)
586         where: 'the following parameters are used'
587             scenario                | operation | datastoreInUrl
588             'unsupported datastore' | 'POST'    | 'ncmp-datastore:operational'
589             'invalid datastore'     | 'POST'    | 'invalid'
590             'unsupported datastore' | 'PUT'     | 'ncmp-datastore:operational'
591             'invalid datastore'     | 'PUT'     | 'invalid'
592             'unsupported datastore' | 'PATCH'   | 'ncmp-datastore:operational'
593             'invalid datastore'     | 'PATCH'   | 'invalid'
594             'unsupported datastore' | 'DELETE'  | 'ncmp-datastore:operational'
595             'invalid datastore'     | 'DELETE'  | 'invalid'
596     }
597
598     def executeRestOperation(operation, url) {
599         if (operation == 'POST') {
600             return post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
601         }
602         if (operation == 'PUT') {
603             return put(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
604         }
605         if (operation == 'PATCH') {
606             return patch(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
607         }
608         if (operation == 'DELETE') {
609             return delete(url).contentType(MediaType.APPLICATION_JSON_VALUE)
610         }
611     }
612
613     def dataStores() {
614         DataStores.builder()
615             .operationalDataStore(Operational.builder()
616                 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
617                 .lastSyncTime(formattedDateAndTime.toString()).build()).build()
618     }
619
620     def compositeStateTestObject() {
621         new CompositeState(cmHandleState: CmHandleState.ADVISED,
622             lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
623             lastUpdateTime: formattedDateAndTime.toString(),
624             dataSyncEnabled: false,
625             dataStores: dataStores())
626     }
627
628     def assertContainsAll(response, assertContent) {
629         assertContent.forEach(string -> { assert (response.contentAsString.contains(string)) })
630         return void
631     }
632
633     def assertContainsState(response) {
634         def expectedContent = [
635             '"state":',
636             '"cmHandleState":"ADVISED"',
637             '"reason":"LOCKED_MISBEHAVING"',
638             '"details":"lock details"',
639             '"lastUpdateTime":"2022-12-31T20:30:40.000+0000"',
640             '"dataSyncEnabled":false',
641             '"dataSyncState":',
642             '"operational":',
643             '"syncState":"NONE_REQUESTED"',
644             '"lastSyncTime":"2022-12-31T20:30:40.000+0000"',
645             '"running":null'
646         ]
647         return assertContainsAll(response, expectedContent)
648     }
649
650     def assertContainsPublicProperties(response) {
651         def expectedContent = [
652             '"publicCmHandleProperties":',
653             '"public prop"',
654             '"some public property"'
655         ]
656         return assertContainsAll(response, expectedContent)
657     }
658
659 }
660