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 requestBody = '{"some-key" : "some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
64 def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
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"
78 def 'Create a node: #scenario.'() {
79 given: 'endpoint to create a node'
80 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
81 when: 'post is invoked with datanode endpoint and json'
85 .contentType(MediaType.APPLICATION_JSON)
86 .param('xpath', parentNodeXpath)
88 ).andReturn().response
89 then: 'a created response is returned'
90 response.status == HttpStatus.CREATED.value()
91 then: 'the java API was called with the correct parameters'
92 1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedJsonData, noTimestamp)
93 where: 'following xpath parameters are are used'
94 scenario | parentNodeXpath
95 'no xpath parameter' | ''
96 'xpath parameter point root' | '/'
99 def 'Create a node with observed-timestamp'() {
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(MediaType.APPLICATION_JSON)
108 .param('observed-timestamp', observedTimestamp)
109 .content(requestBody)
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, expectedJsonData,
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: 'endpoint to create a node'
124 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
125 and: 'parent node xpath'
126 def parentNodeXpath = 'some xpath'
127 when: 'post is invoked with datanode endpoint and json'
128 def postRequestBuilder = post(endpoint)
129 .contentType(MediaType.APPLICATION_JSON)
130 .param('xpath', parentNodeXpath)
131 .content(requestBody)
132 if (observedTimestamp != null)
133 postRequestBuilder.param('observed-timestamp', observedTimestamp)
135 mvc.perform(postRequestBuilder).andReturn().response
136 then: 'a created response is returned'
137 response.status == HttpStatus.CREATED.value()
138 then: 'the java API was called with the correct parameters'
139 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
140 DateTimeUtility.toOffsetDateTime(observedTimestamp))
142 scenario | observedTimestamp
143 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400'
144 'without observed-timestamp' | null
147 def 'Save list elements #scenario.'() {
148 given: 'parent node xpath '
149 def parentNodeXpath = 'parent node xpath'
150 when: 'list-node endpoint is invoked with post (create) operation'
151 def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
152 .contentType(MediaType.APPLICATION_JSON)
153 .param('xpath', parentNodeXpath)
154 .content(requestBody)
155 if (observedTimestamp != null)
156 postRequestBuilder.param('observed-timestamp', observedTimestamp)
157 def response = mvc.perform(postRequestBuilder).andReturn().response
158 then: 'a created response is returned'
159 response.status == expectedHttpStatus.value()
160 then: 'the java API was called with the correct parameters'
161 expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
162 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
164 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
165 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
166 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
167 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
170 def 'Get data node with leaves'() {
171 given: 'the service returns data node leaves'
172 def xpath = 'some xPath'
173 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
174 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
175 when: 'get request is performed through REST API'
177 mvc.perform(get(endpoint).param('xpath', xpath))
178 .andReturn().response
179 then: 'a success response is returned'
180 response.status == HttpStatus.OK.value()
181 and: 'response contains expected leaf and value'
182 response.contentAsString.contains('"leaf":"value"')
183 and: 'response contains expected leaf-list and values'
184 response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
187 def 'Get data node with #scenario.'() {
188 given: 'the service returns data node with #scenario'
189 def xpath = 'some xPath'
190 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
191 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
192 when: 'get request is performed through REST API'
196 .param('xpath', xpath)
197 .param('include-descendants', includeDescendantsOption))
198 .andReturn().response
199 then: 'a success response is returned'
200 response.status == HttpStatus.OK.value()
201 and: 'the response contains child is #expectChildInResponse'
202 response.contentAsString.contains('"child"') == expectChildInResponse
204 scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse
205 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false
206 'no descendant explicitly' | dataNodeWithLeavesNoChildren | 'false' || OMIT_DESCENDANTS | false
207 'with descendants' | dataNodeWithChild | 'true' || INCLUDE_ALL_DESCENDANTS | true
210 def 'Update data node leaves: #scenario.'() {
211 given: 'endpoint to update a node '
212 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
213 when: 'patch request is performed'
217 .contentType(MediaType.APPLICATION_JSON)
218 .content(requestBody)
219 .param('xpath', inputXpath)
220 ).andReturn().response
221 then: 'the service method is invoked with expected parameters'
222 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, null)
223 and: 'response status indicates success'
224 response.status == HttpStatus.OK.value()
226 scenario | inputXpath || xpathServiceParameter
227 'root node by default' | '' || '/'
228 'root node by choice' | '/' || '/'
229 'some xpath by parent' | '/some/xpath' || '/some/xpath'
232 def 'Update data node leaves with observedTimestamp'() {
233 given: 'endpoint to update a node leaves '
234 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
235 when: 'patch request is performed'
239 .contentType(MediaType.APPLICATION_JSON)
240 .content(requestBody)
242 .param('observed-timestamp', observedTimestamp)
243 ).andReturn().response
244 then: 'the service method is invoked with expected parameters'
245 expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData,
246 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
247 and: 'response status indicates success'
248 response.status == expectedHttpStatus.value()
250 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
251 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
252 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
255 def 'Replace data node tree: #scenario.'() {
256 given: 'endpoint to replace node'
257 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
258 when: 'put request is performed'
262 .contentType(MediaType.APPLICATION_JSON)
263 .content(requestBody)
264 .param('xpath', inputXpath))
265 .andReturn().response
266 then: 'the service method is invoked with expected parameters'
267 1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, noTimestamp)
268 and: 'response status indicates success'
269 response.status == HttpStatus.OK.value()
271 scenario | inputXpath || xpathServiceParameter
272 'root node by default' | '' || '/'
273 'root node by choice' | '/' || '/'
274 'some xpath by parent' | '/some/xpath' || '/some/xpath'
277 def 'Replace data node tree with observedTimestamp.'() {
278 given: 'endpoint to replace node'
279 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
280 when: 'put request is performed'
284 .contentType(MediaType.APPLICATION_JSON)
285 .content(requestBody)
287 .param('observed-timestamp', observedTimestamp))
288 .andReturn().response
289 then: 'the service method is invoked with expected parameters'
290 expectedApiCount * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, '/', expectedJsonData,
291 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
292 and: 'response status indicates success'
293 response.status == expectedHttpStatus.value()
295 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
296 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
297 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
300 def 'Replace list content #scenario.'() {
301 when: 'list-nodes endpoint is invoked with put (update) operation'
302 def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
303 .contentType(MediaType.APPLICATION_JSON)
304 .param('xpath', 'parent xpath')
305 .content(requestBody)
306 if (observedTimestamp != null)
307 putRequestBuilder.param('observed-timestamp', observedTimestamp)
308 def response = mvc.perform(putRequestBuilder).andReturn().response
309 then: 'a success response is returned'
310 response.status == expectedHttpStatus.value()
311 and: 'the java API was called with the correct parameters'
312 expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
313 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
315 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
316 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
317 'without observed-timestamp' | null || 1 | HttpStatus.OK
318 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
321 def 'Delete list element #scenario.'() {
322 when: 'list-nodes endpoint is invoked with delete operation'
323 def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
324 .param('xpath', 'list element xpath')
325 if (observedTimestamp != null)
326 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
327 def response = mvc.perform(deleteRequestBuilder).andReturn().response
328 then: 'a success response is returned'
329 response.status == expectedHttpStatus.value()
330 and: 'the java API was called with the correct parameters'
331 expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
332 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
334 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
335 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
336 'without observed-timestamp' | null || 1 | HttpStatus.NO_CONTENT
337 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
340 def 'Delete data node #scenario.'() {
341 given: 'data node xpath'
342 def dataNodeXpath = '/dataNodeXpath'
343 when: 'delete data node endpoint is invoked'
344 def deleteDataNodeRequest = delete( "$dataNodeBaseEndpoint/anchors/$anchorName/nodes")
345 .param('xpath', dataNodeXpath)
346 and: 'observed timestamp is added to the parameters'
347 if (observedTimestamp != null)
348 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
349 def response = mvc.perform(deleteDataNodeRequest).andReturn().response
350 then: 'a successful response is returned'
351 response.status == expectedHttpStatus.value()
352 and: 'the api is called with the correct parameters'
353 expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
354 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
356 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
357 'with observed timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
358 'without observed timestamp' | null || 1 | HttpStatus.NO_CONTENT
359 'with invalid observed timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST