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