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