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