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
12 * http://www.apache.org/licenses/LICENSE-2.0
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.
20 * SPDX-License-Identifier: Apache-2.0
21 * ============LICENSE_END=========================================================
24 package org.onap.cps.rest.controller
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
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
52 @WebMvcTest(DataRestController)
53 class DataRestControllerSpec extends Specification {
56 CpsDataService mockCpsDataService = Mock()
59 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
62 PrefixResolver prefixResolver = Mock()
67 @Value('${rest.api.cps-base-path}')
70 def dataNodeBaseEndpoint
71 def dataspaceName = 'my_dataspace'
72 def anchorName = 'my_anchor'
73 def noTimestamp = null
76 def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
79 def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
82 def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
85 def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
88 static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
89 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
92 static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
93 .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
96 dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
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'
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
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'
129 .contentType(contentType)
131 .param('observed-timestamp', observedTimestamp)
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)
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
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)
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)
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
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) })
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
196 def 'Get data node with leaves'() {
197 given: 'the service returns data node leaves'
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'
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"]')
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'
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
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'
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'
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()
256 scenario | inputXpath || xpathServiceParameter
257 'root node by default' | '' || '/'
258 'root node by choice' | '/' || '/'
259 'some xpath by parent' | '/some/xpath' || '/some/xpath'
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'
269 .contentType(MediaType.APPLICATION_JSON)
270 .content(requestBodyJson)
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()
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
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'
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()
301 scenario | inputXpath || xpathServiceParameter
302 'root node by default' | '' || '/'
303 'root node by choice' | '/' || '/'
304 'some xpath by parent' | '/some/xpath' || '/some/xpath'
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'
314 .contentType(MediaType.APPLICATION_JSON)
315 .content(requestBodyJson)
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()
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
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) })
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
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) })
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
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) })
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