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