API versioning supported and added different versions for POST APIs
[cps.git] / cps-rest / src / test / groovy / org / onap / cps / rest / controller / AdminRestControllerSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2020-2021 Pantheon.tech
4  *  Modifications Copyright (C) 2020-2021 Bell Canada.
5  *  Modifications Copyright (C) 2021-2022 Nordix Foundation
6  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
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
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
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.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.rest.controller
25
26 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED
27 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
28 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
29 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
30 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
31
32 import org.mapstruct.factory.Mappers
33 import org.onap.cps.api.CpsAdminService
34 import org.onap.cps.api.CpsModuleService
35 import org.onap.cps.spi.exceptions.AlreadyDefinedException
36 import org.onap.cps.spi.exceptions.SchemaSetInUseException
37 import org.onap.cps.spi.model.Anchor
38 import org.onap.cps.spi.model.Dataspace
39 import org.onap.cps.spi.model.SchemaSet
40 import org.spockframework.spring.SpringBean
41 import org.springframework.beans.factory.annotation.Autowired
42 import org.springframework.beans.factory.annotation.Value
43 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
44 import org.springframework.http.HttpStatus
45 import org.springframework.http.MediaType
46 import org.springframework.mock.web.MockMultipartFile
47 import org.springframework.test.web.servlet.MockMvc
48 import org.springframework.util.LinkedMultiValueMap
49 import org.springframework.util.MultiValueMap
50 import spock.lang.Specification
51
52 @WebMvcTest(AdminRestController)
53 class AdminRestControllerSpec extends Specification {
54
55     @SpringBean
56     CpsModuleService mockCpsModuleService = Mock()
57
58     @SpringBean
59     CpsAdminService mockCpsAdminService = Mock()
60
61     @SpringBean
62     CpsRestInputMapper cpsRestInputMapper = Mappers.getMapper(CpsRestInputMapper)
63
64     @Autowired
65     MockMvc mvc
66
67     @Value('${rest.api.cps-base-path}')
68     def basePath
69
70     def dataspaceName = 'my_dataspace'
71     def anchorName = 'my_anchor'
72     def schemaSetName = 'my_schema_set'
73     def anchor = new Anchor(name: anchorName, dataspaceName: dataspaceName, schemaSetName: schemaSetName)
74     def dataspace = new Dataspace(name: dataspaceName)
75
76     def 'Create new dataspace with #scenario.'() {
77         when: 'post is invoked'
78             def response =
79                     mvc.perform(
80                             post("/cps/api/${apiVersion}/dataspaces")
81                                     .param('dataspace-name', dataspaceName))
82                             .andReturn().response
83         then: 'service method is invoked with expected parameters'
84             1 * mockCpsAdminService.createDataspace(dataspaceName)
85         and: 'dataspace is create successfully'
86             response.status == HttpStatus.CREATED.value()
87             assert response.getContentAsString() == expectedResponseBody
88         where: 'following cases are tested'
89             scenario | apiVersion  || expectedResponseBody
90             'V1 API' | 'v1'        || 'my_dataspace'
91             'V2 API' | 'v2'        || ''
92     } 
93     def 'Create dataspace over existing with same name.'() {
94         given: 'an endpoint'
95             def createDataspaceEndpoint = "$basePath/v1/dataspaces"
96         and: 'the service method throws an exception indicating the dataspace is already defined'
97             def thrownException = new AlreadyDefinedException(dataspaceName, new RuntimeException())
98             mockCpsAdminService.createDataspace(dataspaceName) >> { throw thrownException }
99         when: 'post is invoked'
100             def response =
101                     mvc.perform(
102                             post(createDataspaceEndpoint)
103                                     .param('dataspace-name', dataspaceName))
104                             .andReturn().response
105         then: 'dataspace creation fails'
106             response.status == HttpStatus.CONFLICT.value()
107     }
108
109     def 'Get a dataspace.'() {
110         given: 'service method returns a dataspace'
111             mockCpsAdminService.getDataspace(dataspaceName) >> dataspace
112         and: 'an endpoint'
113             def getDataspaceEndpoint = "$basePath/v1/admin/dataspaces/$dataspaceName"
114         when: 'get dataspace API is invoked'
115             def response = mvc.perform(get(getDataspaceEndpoint)).andReturn().response
116         then: 'the correct dataspace is returned'
117             response.status == HttpStatus.OK.value()
118             response.getContentAsString().contains(dataspaceName)
119     }
120
121     def 'Get all dataspaces.'() {
122         given: 'service method returns all dataspace'
123             mockCpsAdminService.getAllDataspaces() >> [dataspace, new Dataspace(name: "dataspace-test2")]
124         and: 'an endpoint'
125             def getAllDataspaceEndpoint = "$basePath/v1/admin/dataspaces"
126         when: 'get all dataspace API is invoked'
127             def response = mvc.perform(get(getAllDataspaceEndpoint)).andReturn().response
128         then: 'the correct dataspace is returned'
129             response.status == HttpStatus.OK.value()
130             response.getContentAsString().contains(dataspaceName)
131             response.getContentAsString().contains("dataspace-test2")
132     }
133
134     def 'Create schema set from yang file with #scenario.'() {
135         def yangResourceMapCapture
136         given: 'single yang file'
137             def multipartFile = createMultipartFile("filename.yang", "content")
138         when: 'file uploaded with schema set create request'
139             def response =
140                     mvc.perform(
141                             multipart("/cps/api/${apiVersion}/dataspaces/my_dataspace/schema-sets")
142                                     .file(multipartFile)
143                                     .param('schema-set-name', schemaSetName))
144                             .andReturn().response
145         then: 'associated service method is invoked with expected parameters'
146             1 * mockCpsModuleService.createSchemaSet(dataspaceName, schemaSetName, _) >>
147                     { args -> yangResourceMapCapture = args[2] }
148             yangResourceMapCapture['filename.yang'] == 'content'
149         and: 'response code indicates success'
150             assert response.status == HttpStatus.CREATED.value()
151             assert response.getContentAsString() == expectedResponseBody
152         where: 'following cases are tested'
153             scenario | apiVersion || expectedResponseBody
154             'V1 API' | 'v1'       || 'my_schema_set'
155             'V2 API' | 'v2'       || ''
156     }
157
158     def 'Create schema set from zip archive with #scenario.'() {
159         def yangResourceMapCapture
160         given: 'zip archive with multiple .yang files inside'
161             def multipartFile = createZipMultipartFileFromResource("/yang-files-set.zip")
162         when: 'file uploaded with schema set create request'
163             def response =
164                     mvc.perform(
165                             multipart("/cps/api/${apiVersion}/dataspaces/my_dataspace/schema-sets")
166                                     .file(multipartFile)
167                                     .param('schema-set-name', schemaSetName))
168                             .andReturn().response
169         then: 'associated service method is invoked with expected parameters'
170             1 * mockCpsModuleService.createSchemaSet(dataspaceName, schemaSetName, _) >>
171                     { args -> yangResourceMapCapture = args[2] }
172             yangResourceMapCapture['assembly.yang'] == "fake assembly content 1\n"
173             yangResourceMapCapture['component.yang'] == "fake component content 1\n"
174         and: 'response code indicates success'
175             assert response.status == HttpStatus.CREATED.value()
176             assert response.getContentAsString() == expectedResponseBody
177         where: 'following cases are tested'
178             scenario | apiVersion || expectedResponseBody
179             'V1 API' | 'v1'       || 'my_schema_set'
180             'V2 API' | 'v2'       || ''
181     }
182
183     def 'Create a schema set from a yang file that is greater than 1MB #scenario.'() {
184         given: 'a yang file greater than 1MB'
185             def multipartFile = createMultipartFileFromResource("/model-over-1mb.yang")
186         when: 'a file is uploaded to the create schema set endpoint'
187             def response =
188                     mvc.perform(
189                             multipart("/cps/api/${apiVersion}/dataspaces/my_dataspace/schema-sets")
190                                     .file(multipartFile)
191                                     .param('schema-set-name', schemaSetName))
192                             .andReturn().response
193         then: 'the associated service method is invoked'
194             1 * mockCpsModuleService.createSchemaSet(dataspaceName, schemaSetName, _)
195         and: 'the response code indicates success'
196             assert response.status == HttpStatus.CREATED.value()
197             assert response.getContentAsString() == expectedResponseBody
198         where: 'following cases are tested'
199             scenario | apiVersion || expectedResponseBody
200             'V1 API' | 'v1'       || 'my_schema_set'
201             'V2 API' | 'v2'       || ''
202     }
203
204     def 'Create schema set from zip archive having #caseDescriptor.'() {
205         given: 'an endpoint'
206             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
207         when: 'zip archive having #caseDescriptor is uploaded with create schema set request'
208             def response =
209                     mvc.perform(
210                             multipart(schemaSetEndpoint)
211                                     .file(multipartFile)
212                                     .param('schema-set-name', schemaSetName))
213                             .andReturn().response
214         then: 'create schema set rejected'
215             response.status == HttpStatus.BAD_REQUEST.value()
216         where: 'following cases are tested'
217             caseDescriptor                        | multipartFile
218             'no .yang files inside'               | createZipMultipartFileFromResource("/no-yang-files.zip")
219             'multiple .yang files with same name' | createZipMultipartFileFromResource("/yang-files-multiple-sets.zip")
220     }
221
222     def 'Create schema set from file with unsupported filename extension.'() {
223         given: 'file with unsupported filename extension (.doc)'
224             def multipartFile = createMultipartFile("filename.doc", "content")
225         and: 'an endpoint'
226             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
227         when: 'file uploaded with schema set create request'
228             def response =
229                     mvc.perform(
230                             multipart(schemaSetEndpoint)
231                                     .file(multipartFile)
232                                     .param('schema-set-name', schemaSetName))
233                             .andReturn().response
234         then: 'create schema set rejected'
235             response.status == HttpStatus.BAD_REQUEST.value()
236     }
237
238     def 'Create schema set from #fileType file with IOException occurrence on processing.'() {
239         given: 'an endpoint'
240             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
241         when: 'file uploaded with schema set create request'
242             def multipartFile = createMultipartFileForIOException(fileType)
243             def response =
244                     mvc.perform(
245                             multipart(schemaSetEndpoint)
246                                     .file(multipartFile)
247                                     .param('schema-set-name', schemaSetName))
248                             .andReturn().response
249         then: 'the error response returned indicating internal server error occurrence'
250             response.status == HttpStatus.INTERNAL_SERVER_ERROR.value()
251         where: 'following file types are used'
252             fileType << ['YANG', 'ZIP']
253     }
254
255     def 'Delete schema set.'() {
256         given: 'an endpoint'
257             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets/$schemaSetName"
258         when: 'delete schema set endpoint is invoked'
259             def response = mvc.perform(delete(schemaSetEndpoint)).andReturn().response
260         then: 'associated service method is invoked with expected parameters'
261             1 * mockCpsModuleService.deleteSchemaSet(dataspaceName, schemaSetName, CASCADE_DELETE_PROHIBITED)
262         and: 'response code indicates success'
263             response.status == HttpStatus.NO_CONTENT.value()
264     }
265
266     def 'Delete schema set which is in use.'() {
267         given: 'service method throws an exception indicating the schema set is in use'
268             def thrownException = new SchemaSetInUseException(dataspaceName, schemaSetName)
269             mockCpsModuleService.deleteSchemaSet(dataspaceName, schemaSetName, CASCADE_DELETE_PROHIBITED) >>
270                     { throw thrownException }
271         and: 'an endpoint'
272             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets/$schemaSetName"
273         when: 'delete schema set endpoint is invoked'
274             def response = mvc.perform(delete(schemaSetEndpoint)).andReturn().response
275         then: 'schema set deletion fails with conflict response code'
276             response.status == HttpStatus.CONFLICT.value()
277     }
278
279     def 'Get existing schema set.'() {
280         given: 'service method returns a new schema set'
281             mockCpsModuleService.getSchemaSet(dataspaceName, schemaSetName) >>
282                     new SchemaSet(name: schemaSetName, dataspaceName: dataspaceName)
283         and: 'an endpoint'
284             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets/$schemaSetName"
285         when: 'get schema set API is invoked'
286             def response = mvc.perform(get(schemaSetEndpoint)).andReturn().response
287         then: 'the correct schema set is returned'
288             response.status == HttpStatus.OK.value()
289             response.getContentAsString().contains(schemaSetName)
290     }
291
292     def 'Get all schema sets for a given dataspace name.'() {
293         given: 'service method returns all schema sets for a dataspace'
294             mockCpsModuleService.getSchemaSets(dataspaceName) >>
295                 [new SchemaSet(name: schemaSetName, dataspaceName: dataspaceName),
296                 new SchemaSet(name: "test-schemaset", dataspaceName: dataspaceName)]
297         and: 'an endpoint'
298             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
299         when: 'get schema sets API is invoked'
300             def response = mvc.perform(get(schemaSetEndpoint)).andReturn().response
301         then: 'the correct schema sets is returned'
302             assert response.status == HttpStatus.OK.value()
303             assert response.getContentAsString() == '[{"dataspaceName":"my_dataspace","moduleReferences":[],"name":' +
304                    '"my_schema_set"},{"dataspaceName":"my_dataspace","moduleReferences":[],"name":"test-schemaset"}]'
305     }
306
307     def 'Create Anchor with #scenario.'() {
308         given: 'request parameters'
309             def requestParams = new LinkedMultiValueMap<>()
310             requestParams.add('schema-set-name', schemaSetName)
311             requestParams.add('anchor-name', anchorName)
312         when: 'post is invoked'
313             def response =
314                     mvc.perform(
315                             post("/cps/api/${apiVersion}/dataspaces/my_dataspace/anchors")
316                                     .contentType(MediaType.APPLICATION_JSON)
317                                     .params(requestParams as MultiValueMap))
318                                     .andReturn().response
319         then: 'anchor is created successfully'
320             1 * mockCpsAdminService.createAnchor(dataspaceName, schemaSetName, anchorName)
321             assert response.status == HttpStatus.CREATED.value()
322             assert response.getContentAsString() == expectedResponseBody
323         where: 'following cases are tested'
324             scenario | apiVersion || expectedResponseBody
325             'V1 API' | 'v1'       || 'my_anchor'
326             'V2 API' | 'v2'       || ''
327     }
328
329     def 'Get existing anchor.'() {
330         given: 'service method returns a list of anchors'
331             mockCpsAdminService.getAnchors(dataspaceName) >> [anchor]
332         and: 'an endpoint'
333             def anchorEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors"
334         when: 'get all anchors API is invoked'
335             def response = mvc.perform(get(anchorEndpoint)).andReturn().response
336         then: 'the correct anchor is returned'
337             response.status == HttpStatus.OK.value()
338             response.getContentAsString().contains(anchorName)
339     }
340
341     def 'Get existing anchor by dataspace and anchor name.'() {
342         given: 'service method returns an anchor'
343             mockCpsAdminService.getAnchor(dataspaceName, anchorName) >>
344                     new Anchor(name: anchorName, dataspaceName: dataspaceName, schemaSetName: schemaSetName)
345         and: 'an endpoint'
346             def anchorEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors/$anchorName"
347         when: 'get anchor API is invoked'
348             def response = mvc.perform(get(anchorEndpoint)).andReturn().response
349             def responseContent = response.getContentAsString()
350         then: 'the correct anchor is returned'
351             response.status == HttpStatus.OK.value()
352             responseContent.contains(anchorName)
353             responseContent.contains(dataspaceName)
354             responseContent.contains(schemaSetName)
355     }
356
357     def 'Delete anchor.'() {
358         given: 'an endpoint'
359             def anchorEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors/$anchorName"
360         when: 'delete method is invoked on anchor endpoint'
361             def response = mvc.perform(delete(anchorEndpoint)).andReturn().response
362         then: 'associated service method is invoked with expected parameters'
363             1 * mockCpsAdminService.deleteAnchor(dataspaceName, anchorName)
364         and: 'response code indicates success'
365             response.status == HttpStatus.NO_CONTENT.value()
366     }
367
368     def 'Delete dataspace.'() {
369         given: 'an endpoint'
370             def dataspaceEndpoint = "$basePath/v1/dataspaces"
371         when: 'delete dataspace endpoint is invoked'
372             def response = mvc.perform(delete(dataspaceEndpoint)
373                 .param('dataspace-name', dataspaceName))
374                 .andReturn().response
375         then: 'associated service method is invoked with expected parameter'
376             1 * mockCpsAdminService.deleteDataspace(dataspaceName)
377         and: 'response code indicates success'
378             response.status == HttpStatus.NO_CONTENT.value()
379     }
380
381     def createMultipartFile(filename, content) {
382         return new MockMultipartFile("file", filename, "text/plain", content.getBytes())
383     }
384
385     def createZipMultipartFileFromResource(resourcePath) {
386         return new MockMultipartFile("file", "test.zip", "application/zip",
387                 getClass().getResource(resourcePath).getBytes())
388     }
389
390     def createMultipartFileFromResource(resourcePath) {
391         return new MockMultipartFile("file", "test.yang", "application/text",
392                 getClass().getResource(resourcePath).getBytes())
393     }
394
395     def createMultipartFileForIOException(extension) {
396         def multipartFile = Mock(MockMultipartFile)
397         multipartFile.getOriginalFilename() >> "TEST." + extension
398         multipartFile.getBytes() >> { throw new IOException() }
399         multipartFile.getInputStream() >> { throw new IOException() }
400         return multipartFile
401     }
402
403 }