Added depth parameter in query nodes API.
[cps.git] / cps-rest / src / test / groovy / org / onap / cps / rest / controller / DataRestControllerSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2022 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2021-2022 Bell Canada.
6  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
7  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
8  *  ================================================================================
9  *  Licensed under the Apache License, Version 2.0 (the "License");
10  *  you may not use this file except in compliance with the License.
11  *  You may obtain a copy of the License at
12  *
13  *        http://www.apache.org/licenses/LICENSE-2.0
14  *
15  *  Unless required by applicable law or agreed to in writing, software
16  *  distributed under the License is distributed on an "AS IS" BASIS,
17  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  *  See the License for the specific language governing permissions and
19  *  limitations under the License.
20  *
21  *  SPDX-License-Identifier: Apache-2.0
22  *  ============LICENSE_END=========================================================
23  */
24
25 package org.onap.cps.rest.controller
26
27 import com.fasterxml.jackson.databind.ObjectMapper
28 import org.onap.cps.api.CpsDataService
29 import org.onap.cps.spi.model.DataNode
30 import org.onap.cps.spi.model.DataNodeBuilder
31 import org.onap.cps.utils.ContentType
32 import org.onap.cps.utils.DateTimeUtility
33 import org.onap.cps.utils.JsonObjectMapper
34 import org.onap.cps.utils.PrefixResolver
35 import org.spockframework.spring.SpringBean
36 import org.springframework.beans.factory.annotation.Autowired
37 import org.springframework.beans.factory.annotation.Value
38 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
39 import org.springframework.http.HttpStatus
40 import org.springframework.http.MediaType
41 import org.springframework.test.web.servlet.MockMvc
42 import spock.lang.Shared
43 import spock.lang.Specification
44
45 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
46 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
47 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
48 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
49 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
50 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
51 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
52
53 @WebMvcTest(DataRestController)
54 class DataRestControllerSpec extends Specification {
55
56     @SpringBean
57     CpsDataService mockCpsDataService = Mock()
58
59     @SpringBean
60     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
61
62     @SpringBean
63     PrefixResolver prefixResolver = Mock()
64
65     @Autowired
66     MockMvc mvc
67
68     @Value('${rest.api.cps-base-path}')
69     def basePath
70
71     def dataNodeBaseEndpoint
72     def dataNodeBaseEndpointV2
73     def dataspaceName = 'my_dataspace'
74     def anchorName = 'my_anchor'
75     def noTimestamp = null
76
77     @Shared
78     def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
79
80     @Shared
81     def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
82
83     @Shared
84     def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
85
86     @Shared
87     def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
88
89     @Shared
90     static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
91         .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
92
93     @Shared
94     static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
95         .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
96
97     def setup() {
98         dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
99         dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
100     }
101
102     def 'Create a node: #scenario.'() {
103         given: 'endpoint to create a node'
104             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
105         when: 'post is invoked with datanode endpoint and json'
106             def response =
107                 mvc.perform(
108                     post(endpoint)
109                         .contentType(contentType)
110                         .param('xpath', parentNodeXpath)
111                         .content(requestBody)
112                 ).andReturn().response
113         then: 'a created response is returned'
114             response.status == HttpStatus.CREATED.value()
115         then: 'the java API was called with the correct parameters'
116             1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType)
117         where: 'following xpath parameters are are used'
118             scenario                                   | parentNodeXpath | contentType                | expectedContentType | requestBody     | expectedData
119             'JSON content: no xpath parameter'         | ''              | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
120             'JSON content: xpath parameter point root' | '/'             | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
121             'XML content: no xpath parameter'          | ''              | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
122             'XML content: xpath parameter point root'  | '/'             | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
123     }
124
125     def 'Create a node with observed-timestamp'() {
126         given: 'endpoint to create a node'
127             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
128         when: 'post is invoked with datanode endpoint and json'
129             def response =
130                 mvc.perform(
131                     post(endpoint)
132                         .contentType(contentType)
133                         .param('xpath', '')
134                         .param('observed-timestamp', observedTimestamp)
135                         .content(content)
136                 ).andReturn().response
137         then: 'a created response is returned'
138             response.status == expectedHttpStatus.value()
139         then: 'the java API was called with the correct parameters'
140             expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData,
141                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
142         where:
143             scenario                          | observedTimestamp              | contentType                | content         || expectedApiCount | expectedHttpStatus     | expectedData     | expectedContentType
144             'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
145             'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
146             'with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
147     }
148
149     def 'Create a child node #scenario'() {
150         given: 'endpoint to create a node'
151             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
152         and: 'parent node xpath'
153             def parentNodeXpath = 'some xpath'
154         when: 'post is invoked with datanode endpoint and json'
155             def postRequestBuilder = post(endpoint)
156                 .contentType(contentType)
157                 .param('xpath', parentNodeXpath)
158                 .content(requestBody)
159             if (observedTimestamp != null)
160                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
161             def response =
162                 mvc.perform(postRequestBuilder).andReturn().response
163         then: 'a created response is returned'
164             response.status == HttpStatus.CREATED.value()
165         then: 'the java API was called with the correct parameters'
166             1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData,
167                 DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType)
168         where:
169             scenario                          | observedTimestamp              | contentType                | requestBody     | expectedData     | expectedContentType
170             'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
171             'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
172             'without observed-timestamp JSON' | null                           | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
173             'without observed-timestamp XML'  | null                           | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
174     }
175
176     def 'Save list elements #scenario.'() {
177         given: 'parent node xpath '
178             def parentNodeXpath = 'parent node xpath'
179         when: 'list-node endpoint is invoked with post (create) operation'
180             def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
181                 .contentType(MediaType.APPLICATION_JSON)
182                 .param('xpath', parentNodeXpath)
183                 .content(requestBodyJson)
184             if (observedTimestamp != null)
185                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
186             def response = mvc.perform(postRequestBuilder).andReturn().response
187         then: 'a created response is returned'
188             response.status == expectedHttpStatus.value()
189         then: 'the java API was called with the correct parameters'
190             expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
191                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
192         where:
193             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
194             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.CREATED
195             'without observed-timestamp'      | null                           || 1                | HttpStatus.CREATED
196             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
197     }
198
199     def 'Get data node with leaves'() {
200         given: 'the service returns data node leaves'
201             def xpath = 'xpath'
202             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
203             mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
204         when: 'get request is performed through REST API'
205             def response =
206                 mvc.perform(get(endpoint).param('xpath', xpath))
207                     .andReturn().response
208         then: 'a success response is returned'
209             response.status == HttpStatus.OK.value()
210         then: 'the response contains the the datanode in json format'
211             response.getContentAsString() == '{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}'
212         and: 'response contains expected leaf and value'
213             response.contentAsString.contains('"leaf":"value"')
214         and: 'response contains expected leaf-list and values'
215             response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
216     }
217
218     def 'Get data node with #scenario.'() {
219         given: 'the service returns data node with #scenario'
220             def xpath = 'some xPath'
221             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
222             mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
223         when: 'get request is performed through REST API'
224             def response =
225                 mvc.perform(
226                     get(endpoint)
227                         .param('xpath', xpath)
228                         .param('include-descendants', includeDescendantsOption))
229                     .andReturn().response
230         then: 'a success response is returned'
231             response.status == HttpStatus.OK.value()
232         and: 'the response contains the root node identifier: #expectedRootidentifier'
233             response.contentAsString.contains(expectedRootidentifier)
234         and: 'the response contains child is #expectChildInResponse'
235             response.contentAsString.contains('"child"') == expectChildInResponse
236         where:
237             scenario                    | dataNode                     | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
238             'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'xpath'
239             'no descendant explicitly'  | dataNodeWithLeavesNoChildren | 'false'                  || OMIT_DESCENDANTS             | false                 | 'xpath'
240             'with descendants'          | dataNodeWithChild            | 'true'                   || INCLUDE_ALL_DESCENDANTS      | true                  | 'parent'
241     }
242
243
244     def 'Get data node using v2 api'() {
245         given: 'the service returns data node'
246             def xpath = 'some xPath'
247             def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
248             mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, {descendantsOption -> {
249                 assert descendantsOption.depth == 2}}) >> dataNodeWithChild
250         when: 'get request is performed through REST API'
251             def response =
252                 mvc.perform(
253                         get(endpoint)
254                                 .param('xpath', xpath)
255                                 .param('descendants', '2'))
256                         .andReturn().response
257         then: 'a success response is returned'
258             assert response.status == HttpStatus.OK.value()
259         and: 'the response contains the root node identifier'
260             assert response.contentAsString.contains('parent')
261         and: 'the response contains child is true'
262             assert response.contentAsString.contains('"child"') == true
263     }
264
265     def 'Update data node leaves: #scenario.'() {
266         given: 'endpoint to update a node '
267             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
268         when: 'patch request is performed'
269             def response =
270                 mvc.perform(
271                     patch(endpoint)
272                         .contentType(MediaType.APPLICATION_JSON)
273                         .content(requestBodyJson)
274                         .param('xpath', inputXpath)
275                 ).andReturn().response
276         then: 'the service method is invoked with expected parameters'
277             1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, null)
278         and: 'response status indicates success'
279             response.status == HttpStatus.OK.value()
280         where:
281             scenario               | inputXpath    || xpathServiceParameter
282             'root node by default' | ''            || '/'
283             'root node by choice'  | '/'           || '/'
284             'some xpath by parent' | '/some/xpath' || '/some/xpath'
285     }
286
287     def 'Update data node leaves with observedTimestamp'() {
288         given: 'endpoint to update a node leaves '
289             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
290         when: 'patch request is performed'
291             def response =
292                 mvc.perform(
293                     patch(endpoint)
294                         .contentType(MediaType.APPLICATION_JSON)
295                         .content(requestBodyJson)
296                         .param('xpath', '/')
297                         .param('observed-timestamp', observedTimestamp)
298                 ).andReturn().response
299         then: 'the service method is invoked with expected parameters'
300             expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData,
301                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
302         and: 'response status indicates success'
303             response.status == expectedHttpStatus.value()
304         where:
305             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
306             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
307             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
308     }
309
310     def 'Replace data node tree: #scenario.'() {
311         given: 'endpoint to replace node'
312             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
313         when: 'put request is performed'
314             def response =
315                 mvc.perform(
316                     put(endpoint)
317                         .contentType(MediaType.APPLICATION_JSON)
318                         .content(requestBodyJson)
319                         .param('xpath', inputXpath))
320                     .andReturn().response
321         then: 'the service method is invoked with expected parameters'
322             1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, noTimestamp)
323         and: 'response status indicates success'
324             response.status == HttpStatus.OK.value()
325         where:
326             scenario               | inputXpath    || xpathServiceParameter
327             'root node by default' | ''            || '/'
328             'root node by choice'  | '/'           || '/'
329             'some xpath by parent' | '/some/xpath' || '/some/xpath'
330     }
331
332     def 'Update data node and descendants with observedTimestamp.'() {
333         given: 'endpoint to replace node'
334             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
335         when: 'put request is performed'
336             def response =
337                 mvc.perform(
338                     put(endpoint)
339                         .contentType(MediaType.APPLICATION_JSON)
340                         .content(requestBodyJson)
341                         .param('xpath', '')
342                         .param('observed-timestamp', observedTimestamp))
343                     .andReturn().response
344         then: 'the service method is invoked with expected parameters'
345             expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData,
346                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
347         and: 'response status indicates success'
348             response.status == expectedHttpStatus.value()
349         where:
350             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
351             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
352             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
353     }
354
355     def 'Replace list content #scenario.'() {
356         when: 'list-nodes endpoint is invoked with put (update) operation'
357             def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
358                 .contentType(MediaType.APPLICATION_JSON)
359                 .param('xpath', 'parent xpath')
360                 .content(requestBodyJson)
361             if (observedTimestamp != null)
362                 putRequestBuilder.param('observed-timestamp', observedTimestamp)
363             def response = mvc.perform(putRequestBuilder).andReturn().response
364         then: 'a success response is returned'
365             response.status == expectedHttpStatus.value()
366         and: 'the java API was called with the correct parameters'
367             expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
368                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
369         where:
370             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
371             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
372             'without observed-timestamp'      | null                           || 1                | HttpStatus.OK
373             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
374     }
375
376     def 'Delete list element #scenario.'() {
377         when: 'list-nodes endpoint is invoked with delete operation'
378             def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
379                 .param('xpath', 'list element xpath')
380             if (observedTimestamp != null)
381                 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
382             def response = mvc.perform(deleteRequestBuilder).andReturn().response
383         then: 'a success response is returned'
384             response.status == expectedHttpStatus.value()
385         and: 'the java API was called with the correct parameters'
386             expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
387                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
388         where:
389             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
390             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.NO_CONTENT
391             'without observed-timestamp'      | null                           || 1                | HttpStatus.NO_CONTENT
392             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
393     }
394
395     def 'Delete data node #scenario.'() {
396         given: 'data node xpath'
397             def dataNodeXpath = '/dataNodeXpath'
398         when: 'delete data node endpoint is invoked'
399             def deleteDataNodeRequest = delete( "$dataNodeBaseEndpoint/anchors/$anchorName/nodes")
400                 .param('xpath', dataNodeXpath)
401         and: 'observed timestamp is added to the parameters'
402             if (observedTimestamp != null)
403                 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
404             def response = mvc.perform(deleteDataNodeRequest).andReturn().response
405         then: 'a successful response is returned'
406             response.status == expectedHttpStatus.value()
407         and: 'the api is called with the correct parameters'
408             expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
409                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
410         where:
411             scenario                            | observedTimestamp                 || expectedApiCount | expectedHttpStatus
412             'with observed timestamp'           | '2021-03-03T23:59:59.999-0400'    || 1                | HttpStatus.NO_CONTENT
413             'without observed timestamp'        | null                              || 1                | HttpStatus.NO_CONTENT
414             'with invalid observed timestamp'   | 'invalid'                         || 0                | HttpStatus.BAD_REQUEST
415     }
416 }