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 * ================================================================================
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 com.fasterxml.jackson.databind.ObjectMapper
26 import org.onap.cps.api.CpsDataService
27 import org.onap.cps.spi.model.DataNode
28 import org.onap.cps.spi.model.DataNodeBuilder
29 import org.onap.cps.utils.DateTimeUtility
30 import org.onap.cps.utils.JsonObjectMapper
31 import org.onap.cps.utils.PrefixResolver
32 import org.spockframework.spring.SpringBean
33 import org.springframework.beans.factory.annotation.Autowired
34 import org.springframework.beans.factory.annotation.Value
35 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
36 import org.springframework.http.HttpStatus
37 import org.springframework.http.MediaType
38 import org.springframework.test.web.servlet.MockMvc
39 import spock.lang.Shared
40 import spock.lang.Specification
42 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
43 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
44 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
45 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
46 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
47 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
48 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
50 @WebMvcTest(DataRestController)
51 class DataRestControllerSpec extends Specification {
54 CpsDataService mockCpsDataService = Mock()
57 JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
60 PrefixResolver prefixResolver = Mock()
65 @Value('${rest.api.cps-base-path}')
68 def dataNodeBaseEndpoint
69 def dataspaceName = 'my_dataspace'
70 def anchorName = 'my_anchor'
71 def noTimestamp = null
72 def requestBody = '{"some-key" : "some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
73 def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
76 static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
77 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
80 static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
81 .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
84 dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
87 def 'Create a node: #scenario.'() {
88 given: 'endpoint to create a node'
89 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
90 when: 'post is invoked with datanode endpoint and json'
94 .contentType(MediaType.APPLICATION_JSON)
95 .param('xpath', parentNodeXpath)
97 ).andReturn().response
98 then: 'a created response is returned'
99 response.status == HttpStatus.CREATED.value()
100 then: 'the java API was called with the correct parameters'
101 1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedJsonData, noTimestamp)
102 where: 'following xpath parameters are are used'
103 scenario | parentNodeXpath
104 'no xpath parameter' | ''
105 'xpath parameter point root' | '/'
108 def 'Create a node with observed-timestamp'() {
109 given: 'endpoint to create a node'
110 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
111 when: 'post is invoked with datanode endpoint and json'
115 .contentType(MediaType.APPLICATION_JSON)
117 .param('observed-timestamp', observedTimestamp)
118 .content(requestBody)
119 ).andReturn().response
120 then: 'a created response is returned'
121 response.status == expectedHttpStatus.value()
122 then: 'the java API was called with the correct parameters'
123 expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedJsonData,
124 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
126 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
127 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
128 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
131 def 'Create a child node'() {
132 given: 'endpoint to create a node'
133 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
134 and: 'parent node xpath'
135 def parentNodeXpath = 'some xpath'
136 when: 'post is invoked with datanode endpoint and json'
137 def postRequestBuilder = post(endpoint)
138 .contentType(MediaType.APPLICATION_JSON)
139 .param('xpath', parentNodeXpath)
140 .content(requestBody)
141 if (observedTimestamp != null)
142 postRequestBuilder.param('observed-timestamp', observedTimestamp)
144 mvc.perform(postRequestBuilder).andReturn().response
145 then: 'a created response is returned'
146 response.status == HttpStatus.CREATED.value()
147 then: 'the java API was called with the correct parameters'
148 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
149 DateTimeUtility.toOffsetDateTime(observedTimestamp))
151 scenario | observedTimestamp
152 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400'
153 'without observed-timestamp' | null
156 def 'Save list elements #scenario.'() {
157 given: 'parent node xpath '
158 def parentNodeXpath = 'parent node xpath'
159 when: 'list-node endpoint is invoked with post (create) operation'
160 def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
161 .contentType(MediaType.APPLICATION_JSON)
162 .param('xpath', parentNodeXpath)
163 .content(requestBody)
164 if (observedTimestamp != null)
165 postRequestBuilder.param('observed-timestamp', observedTimestamp)
166 def response = mvc.perform(postRequestBuilder).andReturn().response
167 then: 'a created response is returned'
168 response.status == expectedHttpStatus.value()
169 then: 'the java API was called with the correct parameters'
170 expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
171 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
173 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
174 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
175 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
176 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
179 def 'Get data node with leaves'() {
180 given: 'the service returns data node leaves'
182 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
183 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
184 when: 'get request is performed through REST API'
186 mvc.perform(get(endpoint).param('xpath', xpath))
187 .andReturn().response
188 then: 'a success response is returned'
189 response.status == HttpStatus.OK.value()
190 then: 'the response contains the the datanode in json format'
191 response.getContentAsString() == '{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}'
192 and: 'response contains expected leaf and value'
193 response.contentAsString.contains('"leaf":"value"')
194 and: 'response contains expected leaf-list and values'
195 response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
198 def 'Get data node with #scenario.'() {
199 given: 'the service returns data node with #scenario'
200 def xpath = 'some xPath'
201 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
202 mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
203 when: 'get request is performed through REST API'
207 .param('xpath', xpath)
208 .param('include-descendants', includeDescendantsOption))
209 .andReturn().response
210 then: 'a success response is returned'
211 response.status == HttpStatus.OK.value()
212 and: 'the response contains the root node identifier: #expectedRootidentifier'
213 response.contentAsString.contains(expectedRootidentifier)
214 and: 'the response contains child is #expectChildInResponse'
215 response.contentAsString.contains('"child"') == expectChildInResponse
217 scenario | dataNode | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
218 'no descendants by default' | dataNodeWithLeavesNoChildren | '' || OMIT_DESCENDANTS | false | 'xpath'
219 'no descendant explicitly' | dataNodeWithLeavesNoChildren | 'false' || OMIT_DESCENDANTS | false | 'xpath'
220 'with descendants' | dataNodeWithChild | 'true' || INCLUDE_ALL_DESCENDANTS | true | 'parent'
223 def 'Update data node leaves: #scenario.'() {
224 given: 'endpoint to update a node '
225 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
226 when: 'patch request is performed'
230 .contentType(MediaType.APPLICATION_JSON)
231 .content(requestBody)
232 .param('xpath', inputXpath)
233 ).andReturn().response
234 then: 'the service method is invoked with expected parameters'
235 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, null)
236 and: 'response status indicates success'
237 response.status == HttpStatus.OK.value()
239 scenario | inputXpath || xpathServiceParameter
240 'root node by default' | '' || '/'
241 'root node by choice' | '/' || '/'
242 'some xpath by parent' | '/some/xpath' || '/some/xpath'
245 def 'Update data node leaves with observedTimestamp'() {
246 given: 'endpoint to update a node leaves '
247 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
248 when: 'patch request is performed'
252 .contentType(MediaType.APPLICATION_JSON)
253 .content(requestBody)
255 .param('observed-timestamp', observedTimestamp)
256 ).andReturn().response
257 then: 'the service method is invoked with expected parameters'
258 expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData,
259 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
260 and: 'response status indicates success'
261 response.status == expectedHttpStatus.value()
263 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
264 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
265 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
268 def 'Replace data node tree: #scenario.'() {
269 given: 'endpoint to replace node'
270 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
271 when: 'put request is performed'
275 .contentType(MediaType.APPLICATION_JSON)
276 .content(requestBody)
277 .param('xpath', inputXpath))
278 .andReturn().response
279 then: 'the service method is invoked with expected parameters'
280 1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, noTimestamp)
281 and: 'response status indicates success'
282 response.status == HttpStatus.OK.value()
284 scenario | inputXpath || xpathServiceParameter
285 'root node by default' | '' || '/'
286 'root node by choice' | '/' || '/'
287 'some xpath by parent' | '/some/xpath' || '/some/xpath'
290 def 'Update data node and descendants with observedTimestamp.'() {
291 given: 'endpoint to replace node'
292 def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
293 when: 'put request is performed'
297 .contentType(MediaType.APPLICATION_JSON)
298 .content(requestBody)
300 .param('observed-timestamp', observedTimestamp))
301 .andReturn().response
302 then: 'the service method is invoked with expected parameters'
303 expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData,
304 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
305 and: 'response status indicates success'
306 response.status == expectedHttpStatus.value()
308 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
309 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
310 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
313 def 'Replace list content #scenario.'() {
314 when: 'list-nodes endpoint is invoked with put (update) operation'
315 def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
316 .contentType(MediaType.APPLICATION_JSON)
317 .param('xpath', 'parent xpath')
318 .content(requestBody)
319 if (observedTimestamp != null)
320 putRequestBuilder.param('observed-timestamp', observedTimestamp)
321 def response = mvc.perform(putRequestBuilder).andReturn().response
322 then: 'a success response is returned'
323 response.status == expectedHttpStatus.value()
324 and: 'the java API was called with the correct parameters'
325 expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
326 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
328 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
329 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
330 'without observed-timestamp' | null || 1 | HttpStatus.OK
331 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
334 def 'Delete list element #scenario.'() {
335 when: 'list-nodes endpoint is invoked with delete operation'
336 def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
337 .param('xpath', 'list element xpath')
338 if (observedTimestamp != null)
339 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
340 def response = mvc.perform(deleteRequestBuilder).andReturn().response
341 then: 'a success response is returned'
342 response.status == expectedHttpStatus.value()
343 and: 'the java API was called with the correct parameters'
344 expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
345 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
347 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
348 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
349 'without observed-timestamp' | null || 1 | HttpStatus.NO_CONTENT
350 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
353 def 'Delete data node #scenario.'() {
354 given: 'data node xpath'
355 def dataNodeXpath = '/dataNodeXpath'
356 when: 'delete data node endpoint is invoked'
357 def deleteDataNodeRequest = delete( "$dataNodeBaseEndpoint/anchors/$anchorName/nodes")
358 .param('xpath', dataNodeXpath)
359 and: 'observed timestamp is added to the parameters'
360 if (observedTimestamp != null)
361 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
362 def response = mvc.perform(deleteDataNodeRequest).andReturn().response
363 then: 'a successful response is returned'
364 response.status == expectedHttpStatus.value()
365 and: 'the api is called with the correct parameters'
366 expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
367 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
369 scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
370 'with observed timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
371 'without observed timestamp' | null || 1 | HttpStatus.NO_CONTENT
372 'with invalid observed timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST