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