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 groovy.json.JsonSlurper
29 import org.onap.cps.api.CpsDataService
30 import org.onap.cps.spi.FetchDescendantsOption
31 import org.onap.cps.spi.model.DataNode
32 import org.onap.cps.spi.model.DataNodeBuilder
33 import org.onap.cps.utils.ContentType
34 import org.onap.cps.utils.DateTimeUtility
35 import org.onap.cps.utils.JsonObjectMapper
36 import org.onap.cps.utils.PrefixResolver
37 import org.spockframework.spring.SpringBean
38 import org.springframework.beans.factory.annotation.Autowired
39 import org.springframework.beans.factory.annotation.Value
40 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
41 import org.springframework.http.HttpStatus
42 import org.springframework.http.MediaType
43 import org.springframework.test.web.servlet.MockMvc
44 import spock.lang.Shared
45 import spock.lang.Specification
47 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
48 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
49 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
50 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
51 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
52 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
53 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
55 @WebMvcTest(DataRestController)
56 class DataRestControllerSpec extends Specification {
59 CpsDataService mockCpsDataService = Mock()
62 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
65 PrefixResolver prefixResolver = Mock()
70 @Value('${rest.api.cps-base-path}')
73 def dataNodeBaseEndpointV1
74 def dataNodeBaseEndpointV2
75 def dataspaceName = 'my_dataspace'
76 def anchorName = 'my_anchor'
77 def noTimestamp = null
80 def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
83 def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
86 def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
89 def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
92 static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/parent-1')
93 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
96 static DataNode dataNodeWithLeavesNoChildren2 = new DataNodeBuilder().withXpath('/parent-2')
97 .withLeaves([leaf: 'value']).build()
100 static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
101 .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
104 dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName"
105 dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
108 def 'Create a node: #scenario.'() {
109 given: 'endpoint to create a node'
110 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
111 when: 'post is invoked with datanode endpoint and json'
115 .contentType(contentType)
116 .param('xpath', parentNodeXpath)
117 .content(requestBody)
118 ).andReturn().response
119 then: 'a created response is returned'
120 response.status == HttpStatus.CREATED.value()
121 then: 'the java API was called with the correct parameters'
122 1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType)
123 where: 'following xpath parameters are are used'
124 scenario | parentNodeXpath | contentType | expectedContentType | requestBody | expectedData
125 'JSON content: no xpath parameter' | '' | MediaType.APPLICATION_JSON | ContentType.JSON | requestBodyJson | expectedJsonData
126 'JSON content: xpath parameter point root' | '/' | MediaType.APPLICATION_JSON | ContentType.JSON | requestBodyJson | expectedJsonData
127 'XML content: no xpath parameter' | '' | MediaType.APPLICATION_XML | ContentType.XML | requestBodyXml | expectedXmlData
128 'XML content: xpath parameter point root' | '/' | MediaType.APPLICATION_XML | ContentType.XML | requestBodyXml | expectedXmlData
131 def 'Create a node with observed-timestamp'() {
132 given: 'endpoint to create a node'
133 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
134 when: 'post is invoked with datanode endpoint and json'
138 .contentType(contentType)
140 .param('observed-timestamp', observedTimestamp)
142 ).andReturn().response
143 then: 'a created response is returned'
144 response.status == expectedHttpStatus.value()
145 then: 'the java API was called with the correct parameters'
146 expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData,
147 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
149 scenario | observedTimestamp | contentType | content || expectedApiCount | expectedHttpStatus | expectedData | expectedContentType
150 'with observed-timestamp JSON' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1 | HttpStatus.CREATED | expectedJsonData | ContentType.JSON
151 'with observed-timestamp XML' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML | requestBodyXml || 1 | HttpStatus.CREATED | expectedXmlData | ContentType.XML
152 'with invalid observed-timestamp' | 'invalid' | MediaType.APPLICATION_JSON | requestBodyJson || 0 | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
155 def 'Create a child node #scenario'() {
156 given: 'endpoint to create a node'
157 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
158 and: 'parent node xpath'
159 def parentNodeXpath = 'some xpath'
160 when: 'post is invoked with datanode endpoint and json'
161 def postRequestBuilder = post(endpoint)
162 .contentType(contentType)
163 .param('xpath', parentNodeXpath)
164 .content(requestBody)
165 if (observedTimestamp != null)
166 postRequestBuilder.param('observed-timestamp', observedTimestamp)
168 mvc.perform(postRequestBuilder).andReturn().response
169 then: 'a created response is returned'
170 response.status == HttpStatus.CREATED.value()
171 then: 'the java API was called with the correct parameters'
172 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData,
173 DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType)
175 scenario | observedTimestamp | contentType | requestBody | expectedData | expectedContentType
176 'with observed-timestamp JSON' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
177 'with observed-timestamp XML' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML | requestBodyXml | expectedXmlData | ContentType.XML
178 'without observed-timestamp JSON' | null | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
179 'without observed-timestamp XML' | null | MediaType.APPLICATION_XML | requestBodyXml | expectedXmlData | ContentType.XML
182 def 'Save list elements #scenario.'() {
183 given: 'parent node xpath '
184 def parentNodeXpath = 'parent node xpath'
185 when: 'list-node endpoint is invoked with post (create) operation'
186 def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
187 .contentType(MediaType.APPLICATION_JSON)
188 .param('xpath', parentNodeXpath)
189 .content(requestBodyJson)
190 if (observedTimestamp != null)
191 postRequestBuilder.param('observed-timestamp', observedTimestamp)
192 def response = mvc.perform(postRequestBuilder).andReturn().response
193 then: 'a created response is returned'
194 response.status == expectedHttpStatus.value()
195 then: 'the java API was called with the correct parameters'
196 expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
197 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
199 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
200 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
201 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
202 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
205 def 'Get data node with leaves'() {
206 given: 'the service returns data node leaves'
207 def xpath = 'parent-1'
208 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
209 mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren]
210 when: 'get request is performed through REST API'
212 mvc.perform(get(endpoint).param('xpath', xpath))
213 .andReturn().response
214 then: 'a success response is returned'
215 response.status == HttpStatus.OK.value()
216 then: 'the response contains the the datanode in json format'
217 response.getContentAsString() == '{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}'
218 and: 'response contains expected leaf and value'
219 response.contentAsString.contains('"leaf":"value"')
220 and: 'response contains expected leaf-list and values'
221 response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
224 def 'Get data node with #scenario.'() {
225 given: 'the service returns data node with #scenario'
226 def xpath = 'some xPath'
227 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
228 mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
229 when: 'get request is performed through REST API'
233 .param('xpath', xpath)
234 .param('include-descendants', includeDescendantsOption))
235 .andReturn().response
236 then: 'a success response is returned'
237 response.status == HttpStatus.OK.value()
238 and: 'the response contains the root node identifier: #expectedRootidentifier'
239 response.contentAsString.contains(expectedRootidentifier)
240 and: 'the response contains child is #expectChildInResponse'
241 response.contentAsString.contains('"child"') == expectChildInResponse
243 scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
244 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false | 'parent-1'
245 'no descendant explicitly' | dataNodeWithLeavesNoChildren | 'false' || OMIT_DESCENDANTS | false | 'parent-1'
246 'with descendants' | dataNodeWithChild | 'true' || INCLUDE_ALL_DESCENDANTS | true | 'parent'
249 def 'Get all the data trees as json array with root node xPath using V2'() {
250 given: 'the service returns all data node leaves'
252 def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
253 mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2]
254 when: 'V2 of get request is performed through REST API'
256 mvc.perform(get(endpoint).param('xpath', xpath))
257 .andReturn().response
258 then: 'a success response is returned'
259 response.status == HttpStatus.OK.value()
260 and: 'the response contains the datanode in json array format'
261 response.getContentAsString() == '[{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}},' +
262 '{"parent-2":{"leaf":"value"}}]'
263 and: 'the json array contains expected number of data trees'
264 def numberOfDataTrees = new JsonSlurper().parseText(response.getContentAsString()).iterator().size()
265 assert numberOfDataTrees == 2
268 def 'Get data node with #scenario using V2.'() {
269 given: 'the service returns data nodes with #scenario'
270 def xpath = 'some xPath'
271 def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
272 mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
273 when: 'V2 of get request is performed through REST API'
277 .param('xpath', xpath)
278 .param('descendants', includeDescendantsOption))
279 .andReturn().response
280 then: 'a success response is returned'
281 response.status == HttpStatus.OK.value()
282 and: 'the response contains the root node identifier: #expectedRootidentifier'
283 response.contentAsString.contains(expectedRootidentifier)
284 and: 'the response contains child is #expectChildInResponse'
285 response.contentAsString.contains('"child"') == expectChildInResponse
287 scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
288 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false | 'parent-1'
289 'no descendant explicitly' | dataNodeWithLeavesNoChildren | '0' || OMIT_DESCENDANTS | false | 'parent-1'
290 'with descendants' | dataNodeWithChild | '-1' || INCLUDE_ALL_DESCENDANTS | true | 'parent'
293 def 'Get data node using v2 api'() {
294 given: 'the service returns data node'
295 def xpath = 'some xPath'
296 def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
297 mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, { descendantsOption -> {
298 assert descendantsOption.depth == 2}} as FetchDescendantsOption) >> [dataNodeWithChild]
299 when: 'get request is performed through REST API'
303 .param('xpath', xpath)
304 .param('descendants', '2'))
305 .andReturn().response
306 then: 'a success response is returned'
307 assert response.status == HttpStatus.OK.value()
308 and: 'the response contains the root node identifier'
309 assert response.contentAsString.contains('parent')
310 and: 'the response contains child is true'
311 assert response.contentAsString.contains('"child"') == true
314 def 'Update data node leaves: #scenario.'() {
315 given: 'endpoint to update a node '
316 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
317 when: 'patch request is performed'
321 .contentType(MediaType.APPLICATION_JSON)
322 .content(requestBodyJson)
323 .param('xpath', inputXpath)
324 ).andReturn().response
325 then: 'the service method is invoked with expected parameters'
326 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, null)
327 and: 'response status indicates success'
328 response.status == HttpStatus.OK.value()
330 scenario | inputXpath || xpathServiceParameter
331 'root node by default' | '' || '/'
332 'root node by choice' | '/' || '/'
333 'some xpath by parent' | '/some/xpath' || '/some/xpath'
336 def 'Update data node leaves with observedTimestamp'() {
337 given: 'endpoint to update a node leaves '
338 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
339 when: 'patch request is performed'
343 .contentType(MediaType.APPLICATION_JSON)
344 .content(requestBodyJson)
346 .param('observed-timestamp', observedTimestamp)
347 ).andReturn().response
348 then: 'the service method is invoked with expected parameters'
349 expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData,
350 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
351 and: 'response status indicates success'
352 response.status == expectedHttpStatus.value()
354 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
355 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
356 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
359 def 'Replace data node tree: #scenario.'() {
360 given: 'endpoint to replace node'
361 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
362 when: 'put request is performed'
366 .contentType(MediaType.APPLICATION_JSON)
367 .content(requestBodyJson)
368 .param('xpath', inputXpath))
369 .andReturn().response
370 then: 'the service method is invoked with expected parameters'
371 1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, noTimestamp)
372 and: 'response status indicates success'
373 response.status == HttpStatus.OK.value()
375 scenario | inputXpath || xpathServiceParameter
376 'root node by default' | '' || '/'
377 'root node by choice' | '/' || '/'
378 'some xpath by parent' | '/some/xpath' || '/some/xpath'
381 def 'Update data node and descendants with observedTimestamp.'() {
382 given: 'endpoint to replace node'
383 def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
384 when: 'put request is performed'
388 .contentType(MediaType.APPLICATION_JSON)
389 .content(requestBodyJson)
391 .param('observed-timestamp', observedTimestamp))
392 .andReturn().response
393 then: 'the service method is invoked with expected parameters'
394 expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData,
395 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
396 and: 'response status indicates success'
397 response.status == expectedHttpStatus.value()
399 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
400 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
401 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
404 def 'Replace list content #scenario.'() {
405 when: 'list-nodes endpoint is invoked with put (update) operation'
406 def putRequestBuilder = put("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
407 .contentType(MediaType.APPLICATION_JSON)
408 .param('xpath', 'parent xpath')
409 .content(requestBodyJson)
410 if (observedTimestamp != null)
411 putRequestBuilder.param('observed-timestamp', observedTimestamp)
412 def response = mvc.perform(putRequestBuilder).andReturn().response
413 then: 'a success response is returned'
414 response.status == expectedHttpStatus.value()
415 and: 'the java API was called with the correct parameters'
416 expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
417 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
419 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
420 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
421 'without observed-timestamp' | null || 1 | HttpStatus.OK
422 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
425 def 'Delete list element #scenario.'() {
426 when: 'list-nodes endpoint is invoked with delete operation'
427 def deleteRequestBuilder = delete("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
428 .param('xpath', 'list element xpath')
429 if (observedTimestamp != null)
430 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
431 def response = mvc.perform(deleteRequestBuilder).andReturn().response
432 then: 'a success response is returned'
433 response.status == expectedHttpStatus.value()
434 and: 'the java API was called with the correct parameters'
435 expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
436 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
438 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
439 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
440 'without observed-timestamp' | null || 1 | HttpStatus.NO_CONTENT
441 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
444 def 'Delete data node #scenario.'() {
445 given: 'data node xpath'
446 def dataNodeXpath = '/dataNodeXpath'
447 when: 'delete data node endpoint is invoked'
448 def deleteDataNodeRequest = delete( "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes")
449 .param('xpath', dataNodeXpath)
450 and: 'observed timestamp is added to the parameters'
451 if (observedTimestamp != null)
452 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
453 def response = mvc.perform(deleteDataNodeRequest).andReturn().response
454 then: 'a successful response is returned'
455 response.status == expectedHttpStatus.value()
456 and: 'the api is called with the correct parameters'
457 expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
458 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
460 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
461 'with observed timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
462 'without observed timestamp' | null || 1 | HttpStatus.NO_CONTENT
463 'with invalid observed timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST