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