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
11 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
23 package org.onap.cps.rest.controller
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
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
47 @WebMvcTest(DataRestController)
48 class DataRestControllerSpec extends Specification {
51 CpsDataService mockCpsDataService = Mock()
56 @Value('${rest.api.cps-base-path}')
59 def dataNodeBaseEndpoint
60 def dataspaceName = 'my_dataspace'
61 def anchorName = 'my_anchor'
62 def noTimestamp = null
63 def jsonString = '{"some-key" : "some-value"}'
67 static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
68 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
71 static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
72 .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
75 dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
76 jsonObject = groovy.json.JsonOutput.toJson(jsonString);
79 def 'Create a node: #scenario.'() {
80 given: 'endpoint to create a node'
81 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
82 when: 'post is invoked with datanode endpoint and json'
86 .contentType(MediaType.APPLICATION_JSON)
87 .param('xpath', parentNodeXpath)
89 ).andReturn().response
90 then: 'a created response is returned'
91 response.status == HttpStatus.CREATED.value()
92 then: 'the java API was called with the correct parameters'
93 1 * mockCpsDataService.saveData(dataspaceName, anchorName, jsonString, noTimestamp)
94 where: 'following xpath parameters are are used'
95 scenario | parentNodeXpath
96 'no xpath parameter' | ''
97 'xpath parameter point root' | '/'
100 def 'Create a node with observed-timestamp'() {
101 given: 'endpoint to create a node'
102 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
103 when: 'post is invoked with datanode endpoint and json'
107 .contentType(MediaType.APPLICATION_JSON)
109 .param('observed-timestamp', observedTimestamp)
111 ).andReturn().response
112 then: 'a created response is returned'
113 response.status == expectedHttpStatus.value()
114 then: 'the java API was called with the correct parameters'
115 expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, jsonString,
116 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
118 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
119 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
120 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
123 def 'Create a child node'() {
124 given: 'endpoint to create a node'
125 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
126 and: 'parent node xpath'
127 def parentNodeXpath = 'some xpath'
128 when: 'post is invoked with datanode endpoint and json'
129 def postRequestBuilder = post(endpoint)
130 .contentType(MediaType.APPLICATION_JSON)
131 .param('xpath', parentNodeXpath)
133 if (observedTimestamp != null)
134 postRequestBuilder.param('observed-timestamp', observedTimestamp)
136 mvc.perform(postRequestBuilder).andReturn().response
137 then: 'a created response is returned'
138 response.status == HttpStatus.CREATED.value()
139 then: 'the java API was called with the correct parameters'
140 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, jsonString,
141 DateTimeUtility.toOffsetDateTime(observedTimestamp))
143 scenario | observedTimestamp
144 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400'
145 'without observed-timestamp' | null
148 def 'Save list elements #scenario.'() {
149 given: 'parent node xpath '
150 def parentNodeXpath = 'parent node xpath'
151 when: 'list-node endpoint is invoked with post (create) operation'
152 def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
153 .contentType(MediaType.APPLICATION_JSON)
154 .param('xpath', parentNodeXpath)
156 if (observedTimestamp != null)
157 postRequestBuilder.param('observed-timestamp', observedTimestamp)
158 def response = mvc.perform(postRequestBuilder).andReturn().response
159 then: 'a created response is returned'
160 response.status == expectedHttpStatus.value()
161 then: 'the java API was called with the correct parameters'
162 expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, jsonString,
163 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
165 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
166 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
167 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
168 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
171 def 'Get data node with leaves'() {
172 given: 'the service returns data node leaves'
173 def xpath = 'some xPath'
174 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
175 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
176 when: 'get request is performed through REST API'
178 mvc.perform(get(endpoint).param('xpath', xpath))
179 .andReturn().response
180 then: 'a success response is returned'
181 response.status == HttpStatus.OK.value()
182 and: 'response contains expected leaf and value'
183 response.contentAsString.contains('"leaf":"value"')
184 and: 'response contains expected leaf-list and values'
185 response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
188 def 'Get data node with #scenario.'() {
189 given: 'the service returns data node with #scenario'
190 def xpath = 'some xPath'
191 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
192 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
193 when: 'get request is performed through REST API'
197 .param('xpath', xpath)
198 .param('include-descendants', includeDescendantsOption))
199 .andReturn().response
200 then: 'a success response is returned'
201 response.status == HttpStatus.OK.value()
202 and: 'the response contains child is #expectChildInResponse'
203 response.contentAsString.contains('"child"') == expectChildInResponse
205 scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse
206 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false
207 'no descendant explicitly' | dataNodeWithLeavesNoChildren | 'false' || OMIT_DESCENDANTS | false
208 'with descendants' | dataNodeWithChild | 'true' || INCLUDE_ALL_DESCENDANTS | true
211 def 'Update data node leaves: #scenario.'() {
212 given: 'endpoint to update a node '
213 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
214 when: 'patch request is performed'
218 .contentType(MediaType.APPLICATION_JSON)
220 .param('xpath', inputXpath)
221 ).andReturn().response
222 then: 'the service method is invoked with expected parameters'
223 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, jsonString, null)
224 and: 'response status indicates success'
225 response.status == HttpStatus.OK.value()
227 scenario | inputXpath || xpathServiceParameter
228 'root node by default' | '' || '/'
229 'root node by choice' | '/' || '/'
230 'some xpath by parent' | '/some/xpath' || '/some/xpath'
233 def 'Update data node leaves with observedTimestamp'() {
234 given: 'endpoint to update a node leaves '
235 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
236 when: 'patch request is performed'
240 .contentType(MediaType.APPLICATION_JSON)
243 .param('observed-timestamp', observedTimestamp)
244 ).andReturn().response
245 then: 'the service method is invoked with expected parameters'
246 expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', jsonString,
247 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
248 and: 'response status indicates success'
249 response.status == expectedHttpStatus.value()
251 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
252 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
253 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
256 def 'Replace data node tree: #scenario.'() {
257 given: 'endpoint to replace node'
258 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
259 when: 'put request is performed'
263 .contentType(MediaType.APPLICATION_JSON)
265 .param('xpath', inputXpath))
266 .andReturn().response
267 then: 'the service method is invoked with expected parameters'
268 1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, jsonString, noTimestamp)
269 and: 'response status indicates success'
270 response.status == HttpStatus.OK.value()
272 scenario | inputXpath || xpathServiceParameter
273 'root node by default' | '' || '/'
274 'root node by choice' | '/' || '/'
275 'some xpath by parent' | '/some/xpath' || '/some/xpath'
278 def 'Replace data node tree with observedTimestamp.'() {
279 given: 'endpoint to replace node'
280 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
281 when: 'put request is performed'
285 .contentType(MediaType.APPLICATION_JSON)
288 .param('observed-timestamp', observedTimestamp))
289 .andReturn().response
290 then: 'the service method is invoked with expected parameters'
291 expectedApiCount * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, '/', jsonString,
292 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
293 and: 'response status indicates success'
294 response.status == expectedHttpStatus.value()
296 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
297 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
298 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
301 def 'Replace list content #scenario.'() {
302 when: 'list-nodes endpoint is invoked with put (update) operation'
303 def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
304 .contentType(MediaType.APPLICATION_JSON)
305 .param('xpath', 'parent xpath')
307 if (observedTimestamp != null)
308 putRequestBuilder.param('observed-timestamp', observedTimestamp)
309 def response = mvc.perform(putRequestBuilder).andReturn().response
310 then: 'a success response is returned'
311 response.status == expectedHttpStatus.value()
312 and: 'the java API was called with the correct parameters'
313 expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', jsonString,
314 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
316 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
317 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
318 'without observed-timestamp' | null || 1 | HttpStatus.OK
319 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
322 def 'Delete list element #scenario.'() {
323 when: 'list-nodes endpoint is invoked with delete operation'
324 def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
325 .param('xpath', 'list element xpath')
326 if (observedTimestamp != null)
327 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
328 def response = mvc.perform(deleteRequestBuilder).andReturn().response
329 then: 'a success response is returned'
330 response.status == expectedHttpStatus.value()
331 and: 'the java API was called with the correct parameters'
332 expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
333 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
335 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
336 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
337 'without observed-timestamp' | null || 1 | HttpStatus.NO_CONTENT
338 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
341 def 'Delete data node #scenario.'() {
342 given: 'data node xpath'
343 def dataNodeXpath = '/dataNodeXpath'
344 when: 'delete data node endpoint is invoked'
345 def deleteDataNodeRequest = delete( "$dataNodeBaseEndpoint/anchors/$anchorName/nodes")
346 .param('xpath', dataNodeXpath)
347 and: 'observed timestamp is added to the parameters'
348 if (observedTimestamp != null)
349 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
350 def response = mvc.perform(deleteDataNodeRequest).andReturn().response
351 then: 'a successful response is returned'
352 response.status == expectedHttpStatus.value()
353 and: 'the api is called with the correct parameters'
354 expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
355 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
357 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
358 'with observed timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
359 'without observed timestamp' | null || 1 | HttpStatus.NO_CONTENT
360 'with invalid observed timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST