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
65 static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
66 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
69 static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
70 .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
73 dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
76 def 'Create a node: #scenario.'() {
77 given: 'some json to create a data node'
78 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
79 def json = 'some json (this is not validated)'
80 when: 'post is invoked with datanode endpoint and json'
84 .contentType(MediaType.APPLICATION_JSON)
85 .param('xpath', parentNodeXpath)
87 ).andReturn().response
88 then: 'a created response is returned'
89 response.status == HttpStatus.CREATED.value()
90 then: 'the java API was called with the correct parameters'
91 1 * mockCpsDataService.saveData(dataspaceName, anchorName, json, noTimestamp)
92 where: 'following xpath parameters are are used'
93 scenario | parentNodeXpath
94 'no xpath parameter' | ''
95 'xpath parameter point root' | '/'
98 def 'Create a node with observed-timestamp'() {
99 given: 'some json to create a data node'
100 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
101 def json = 'some json (this is not validated)'
102 when: 'post is invoked with datanode endpoint and json'
106 .contentType(MediaType.APPLICATION_JSON)
108 .param('observed-timestamp', observedTimestamp)
110 ).andReturn().response
111 then: 'a created response is returned'
112 response.status == expectedHttpStatus.value()
113 then: 'the java API was called with the correct parameters'
114 expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, json,
115 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
117 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
118 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
119 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
122 def 'Create a child node'() {
123 given: 'some json to create a data node'
124 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
125 def json = 'some json (this is not validated)'
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, json,
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 and json data inputs'
150 def parentNodeXpath = 'parent node xpath'
151 def jsonData = 'json data'
152 when: 'list-node endpoint is invoked with post (create) operation'
153 def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
154 .contentType(MediaType.APPLICATION_JSON)
155 .param('xpath', parentNodeXpath)
157 if (observedTimestamp != null)
158 postRequestBuilder.param('observed-timestamp', observedTimestamp)
159 def response = mvc.perform(postRequestBuilder).andReturn().response
160 then: 'a created response is returned'
161 response.status == expectedHttpStatus.value()
162 then: 'the java API was called with the correct parameters'
163 expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, jsonData,
164 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
166 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
167 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
168 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
169 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
172 def 'Get data node with leaves'() {
173 given: 'the service returns data node leaves'
174 def xpath = 'some xPath'
175 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
176 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
177 when: 'get request is performed through REST API'
179 mvc.perform(get(endpoint).param('xpath', xpath))
180 .andReturn().response
181 then: 'a success response is returned'
182 response.status == HttpStatus.OK.value()
183 and: 'response contains expected leaf and value'
184 response.contentAsString.contains('"leaf":"value"')
185 and: 'response contains expected leaf-list and values'
186 response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
189 def 'Get data node with #scenario.'() {
190 given: 'the service returns data node with #scenario'
191 def xpath = 'some xPath'
192 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
193 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
194 when: 'get request is performed through REST API'
198 .param('xpath', xpath)
199 .param('include-descendants', includeDescendantsOption))
200 .andReturn().response
201 then: 'a success response is returned'
202 response.status == HttpStatus.OK.value()
203 and: 'the response contains child is #expectChildInResponse'
204 response.contentAsString.contains('"child"') == expectChildInResponse
206 scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse
207 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false
208 'no descendant explicitly' | dataNodeWithLeavesNoChildren | 'false' || OMIT_DESCENDANTS | false
209 'with descendants' | dataNodeWithChild | 'true' || INCLUDE_ALL_DESCENDANTS | true
212 def 'Update data node leaves: #scenario.'() {
214 def jsonData = 'json data'
215 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
216 when: 'patch request is performed'
220 .contentType(MediaType.APPLICATION_JSON)
222 .param('xpath', inputXpath)
223 ).andReturn().response
224 then: 'the service method is invoked with expected parameters'
225 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, jsonData, null)
226 and: 'response status indicates success'
227 response.status == HttpStatus.OK.value()
229 scenario | inputXpath || xpathServiceParameter
230 'root node by default' | '' || '/'
231 'root node by choice' | '/' || '/'
232 'some xpath by parent' | '/some/xpath' || '/some/xpath'
235 def 'Update data node leaves with observedTimestamp'() {
237 def jsonData = 'json data'
238 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
239 when: 'patch request is performed'
243 .contentType(MediaType.APPLICATION_JSON)
246 .param('observed-timestamp', observedTimestamp)
247 ).andReturn().response
248 then: 'the service method is invoked with expected parameters'
249 expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', jsonData,
250 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
251 and: 'response status indicates success'
252 response.status == expectedHttpStatus.value()
254 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
255 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
256 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
259 def 'Replace data node tree: #scenario.'() {
261 def jsonData = 'json data'
262 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
263 when: 'put request is performed'
267 .contentType(MediaType.APPLICATION_JSON)
269 .param('xpath', inputXpath))
270 .andReturn().response
271 then: 'the service method is invoked with expected parameters'
272 1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, jsonData, noTimestamp)
273 and: 'response status indicates success'
274 response.status == HttpStatus.OK.value()
276 scenario | inputXpath || xpathServiceParameter
277 'root node by default' | '' || '/'
278 'root node by choice' | '/' || '/'
279 'some xpath by parent' | '/some/xpath' || '/some/xpath'
282 def 'Replace data node tree with observedTimestamp.'() {
284 def jsonData = 'json data'
285 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
286 when: 'put request is performed'
290 .contentType(MediaType.APPLICATION_JSON)
293 .param('observed-timestamp', observedTimestamp))
294 .andReturn().response
295 then: 'the service method is invoked with expected parameters'
296 expectedApiCount * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, '/', jsonData,
297 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
298 and: 'response status indicates success'
299 response.status == expectedHttpStatus.value()
301 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
302 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
303 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
306 def 'Replace list content #scenario.'() {
307 when: 'list-nodes endpoint is invoked with put (update) operation'
308 def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
309 .contentType(MediaType.APPLICATION_JSON)
310 .param('xpath', 'parent xpath')
311 .content('json data')
312 if (observedTimestamp != null)
313 putRequestBuilder.param('observed-timestamp', observedTimestamp)
314 def response = mvc.perform(putRequestBuilder).andReturn().response
315 then: 'a success response is returned'
316 response.status == expectedHttpStatus.value()
317 and: 'the java API was called with the correct parameters'
318 expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', 'json data',
319 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
321 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
322 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
323 'without observed-timestamp' | null || 1 | HttpStatus.OK
324 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
327 def 'Delete list element #scenario.'() {
328 when: 'list-nodes endpoint is invoked with delete operation'
329 def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
330 .param('xpath', 'list element xpath')
331 if (observedTimestamp != null)
332 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
333 def response = mvc.perform(deleteRequestBuilder).andReturn().response
334 then: 'a success response is returned'
335 response.status == expectedHttpStatus.value()
336 and: 'the java API was called with the correct parameters'
337 expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
338 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
340 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
341 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
342 'without observed-timestamp' | null || 1 | HttpStatus.NO_CONTENT
343 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
346 def 'Delete data node #scenario.'() {
347 given: 'data node xpath'
348 def dataNodeXpath = '/dataNodeXpath'
349 when: 'delete data node endpoint is invoked'
350 def deleteDataNodeRequest = delete( "$dataNodeBaseEndpoint/anchors/$anchorName/nodes")
351 .param('xpath', dataNodeXpath)
352 and: 'observed timestamp is added to the parameters'
353 if (observedTimestamp != null)
354 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
355 def response = mvc.perform(deleteDataNodeRequest).andReturn().response
356 then: 'a successful response is returned'
357 response.status == expectedHttpStatus.value()
358 and: 'the api is called with the correct parameters'
359 expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
360 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
362 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
363 'with observed timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
364 'without observed timestamp' | null || 1 | HttpStatus.NO_CONTENT
365 'with invalid observed timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST