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