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