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
13 * http://www.apache.org/licenses/LICENSE-2.0
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.
21 * SPDX-License-Identifier: Apache-2.0
22 * ============LICENSE_END=========================================================
25 package org.onap.cps.rest.controller
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
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
53 @WebMvcTest(DataRestController)
54 class DataRestControllerSpec extends Specification {
57 CpsDataService mockCpsDataService = Mock()
60 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
63 PrefixResolver prefixResolver = Mock()
68 @Value('${rest.api.cps-base-path}')
71 def dataNodeBaseEndpoint
72 def dataNodeBaseEndpointV2
73 def dataspaceName = 'my_dataspace'
74 def anchorName = 'my_anchor'
75 def noTimestamp = null
78 def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
81 def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
84 def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
87 def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
90 static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
91 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
94 static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
95 .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
98 dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
99 dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
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'
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
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'
132 .contentType(contentType)
134 .param('observed-timestamp', observedTimestamp)
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)
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
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)
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)
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
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) })
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
199 def 'Get data node with leaves'() {
200 given: 'the service returns data node leaves'
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'
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"]')
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'
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
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'
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'
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
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'
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()
281 scenario | inputXpath || xpathServiceParameter
282 'root node by default' | '' || '/'
283 'root node by choice' | '/' || '/'
284 'some xpath by parent' | '/some/xpath' || '/some/xpath'
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'
294 .contentType(MediaType.APPLICATION_JSON)
295 .content(requestBodyJson)
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()
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
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'
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()
326 scenario | inputXpath || xpathServiceParameter
327 'root node by default' | '' || '/'
328 'root node by choice' | '/' || '/'
329 'some xpath by parent' | '/some/xpath' || '/some/xpath'
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'
339 .contentType(MediaType.APPLICATION_JSON)
340 .content(requestBodyJson)
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()
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
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) })
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
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) })
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
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) })
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