89fe5ec0fc08efae714a8fc0c13c27806db0aa75
[cps.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
4  *  ================================================================================
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.ncmp.impl.data.policyexecutor
22
23 import com.fasterxml.jackson.core.JsonProcessingException
24 import com.fasterxml.jackson.databind.ObjectMapper
25 import org.onap.cps.ncmp.api.exceptions.NcmpException
26 import org.onap.cps.ncmp.api.exceptions.ProvMnSException
27 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
28 import org.onap.cps.ncmp.impl.provmns.RequestPathParameters;
29 import org.onap.cps.ncmp.impl.provmns.model.PatchItem;
30 import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf
31 import org.onap.cps.utils.JsonObjectMapper;
32 import spock.lang.Specification;
33
34 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
35 import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE;
36 import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE;
37
38 class OperationDetailsFactorySpec extends Specification {
39
40     def spiedObjectMapper = Spy(ObjectMapper)
41     def jsonObjectMapper = new JsonObjectMapper(spiedObjectMapper)
42     def policyExecutor = Mock(PolicyExecutor)
43
44     OperationDetailsFactory objectUnderTest = new OperationDetailsFactory(jsonObjectMapper, spiedObjectMapper, policyExecutor)
45
46     static def complexValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['myAttribute1:myValue1', 'myAttribute2:myValue2'], objectClass: 'myClassName')
47     static def simpleValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['simpleAttribute:1'], objectClass: 'myClassName')
48     static def yangModelCmHandle = new YangModelCmHandle(id: 'someId')
49
50     def 'Build create operation details with all properties.'() {
51         given: 'request parameters and resource'
52             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'my id')
53             def resource = new ResourceOneOf(id: 'some resource id', objectClass: 'class in resource')
54         when: 'create operation details are built'
55             def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
56         then: 'all details are correct'
57             assert result.targetIdentifier == 'my uri'
58             assert result.changeRequest.keySet()[0] == 'class in resource'
59             assert result.changeRequest['class in resource'][0].id == 'my id'
60     }
61
62     def 'Build replace operation details with all properties where class name in body is #scenario.'() {
63         given: 'request parameters and resource'
64             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'some id')
65             def resource = new ResourceOneOf(id: 'some resource id', objectClass: classNameInBody)
66         when: 'replace operation details are built'
67             def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
68         then: 'all details are correct'
69             assert result.targetIdentifier == 'my uri'
70             assert result.changeRequest.keySet()[0] == expectedChangeRequestKey
71         where:
72             scenario    | classNameInBody || expectedChangeRequestKey
73             'populated' | 'class in body' || 'class in body'
74             'empty'     | ''              || 'class in uri'
75             'null'      | null            || 'class in uri'
76     }
77
78     def 'Build delete operation details with all properties'() {
79         given: 'request parameters'
80             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'my uri', className: 'classNameInUri', id: 'myId')
81         when: 'delete operation details are built'
82             def result = objectUnderTest.buildDeleteOperationDetails(requestPathParameters.toAlternateId())
83         then: 'all details are correct'
84             assert result.targetIdentifier == 'my uri/classNameInUri=myId'
85     }
86
87     def 'Single patch operation with #patchOperationType checks correct operation type.'() {
88         given: 'request parameters and single patch item'
89             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'some uri', className: 'some class')
90             def resource = new ResourceOneOf(id: 'some resource id')
91             def patchItem = new PatchItem(op: patchOperationType, 'path':'some uri', value: resource)
92         when: 'patch operation is processed'
93             objectUnderTest.checkPermissionForEachPatchItem(requestPathParameters, [patchItem], yangModelCmHandle)
94         then: 'policy executor is called with correct operation type'
95             1 * policyExecutor.checkPermission(yangModelCmHandle, expectedPolicyExecutorOperationType, _, _, _)
96         where: 'following operations are used'
97             patchOperationType | expectedPolicyExecutorOperationType
98             'ADD'              | CREATE
99             'REPLACE'          | UPDATE
100     }
101
102     def 'Single patch operation with REMOVE checks correct operation type.'() {
103         given: 'request parameters and single remove patch item'
104             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'some uri')
105             def patchItem = new PatchItem(op: 'REMOVE')
106         when: 'patch operation is processed'
107             objectUnderTest.checkPermissionForEachPatchItem(requestPathParameters, [patchItem], yangModelCmHandle)
108         then: 'policy executor is called with DELETE operation type'
109             1 * policyExecutor.checkPermission(yangModelCmHandle, DELETE, _, _, _)
110     }
111
112     def 'Multiple patch operations invoke policy executor correct number of times in order.'() {
113         given: 'request parameters and multiple patch items'
114             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'some uri')
115             def resource = new ResourceOneOf(id: 'some resource id', objectClass: 'some class')
116             def patchItemsList = [
117                 new PatchItem(op: 'ADD', 'path':'some uri', value: resource),
118                 new PatchItem(op: 'REPLACE', 'path':'some uri', value: resource),
119                 new PatchItem(op: 'REMOVE', 'path':'some uri')
120             ]
121         when: 'patch operations are processed'
122             objectUnderTest.checkPermissionForEachPatchItem(requestPathParameters, patchItemsList, yangModelCmHandle)
123         then: 'policy executor is checked for create first'
124             1 * policyExecutor.checkPermission(yangModelCmHandle, CREATE, _, _, _)
125         then: 'update is next'
126             1 * policyExecutor.checkPermission(yangModelCmHandle, UPDATE, _, _, _)
127         then: 'and finally delete'
128             1 * policyExecutor.checkPermission(yangModelCmHandle, DELETE, _, _, _)
129     }
130
131     def 'Build policy executor patch operation details with single replace operation and #scenario.'() {
132         given: 'a requestParameter and a patchItem list'
133             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'some uri', className: 'some class')
134             def pathItems = [new PatchItem(op: 'REPLACE', 'path':"some uri${suffix}", value: value)]
135         when: 'patch operation details are checked'
136             objectUnderTest.checkPermissionForEachPatchItem(requestPathParameters, pathItems, yangModelCmHandle)
137         then: 'policyExecutor is called with correct payload'
138             1 * policyExecutor.checkPermission(
139                     yangModelCmHandle,
140                     UPDATE,
141                     null,
142                     requestPathParameters.toAlternateId(),
143                     { json -> assert json.contains(attributesValueInOperation) } // check for more details eg. verify type
144             )
145         where: 'attributes are set using # or resource'
146             scenario                           | suffix                         | value                  || attributesValueInOperation
147             'set simple value using #'         | '#/attributes/simpleAttribute' | 1                      || '{"simpleAttribute":1}'
148             'set simple value using resource'  | ''                             | simpleValueAsResource  || '["simpleAttribute:1"]'
149             'set complex value using resource' | ''                             | complexValueAsResource || '["myAttribute1:myValue1","myAttribute2:myValue2"]'
150     }
151
152     def 'Build an attribute map with different depths of hierarchy with #scenario.'() {
153         given: 'a patch item with a path'
154             def patchItem = new PatchItem(op: 'REPLACE', 'path':path, value: 123)
155         when: 'transforming the attributes'
156             def hierarchyMap = objectUnderTest.createNestedMap(patchItem)
157         then: 'the map depth is equal to the expected number of attributes'
158             assert hierarchyMap.get(expectedAttributeName).toString() == expectedAttributeValue
159         where: 'simple and complex attributes are tested'
160             scenario                                   | path                                                             || expectedAttributeName || expectedAttributeValue
161             'set a simple attribute'                   | 'myUriLdnFirstPart#/attributes/simpleAttribute'                  || 'simpleAttribute'     || '123'
162             'set a simple attribute with a trailing /' | 'myUriLdnFirstPart#/attributes/simpleAttribute/'                 || 'simpleAttribute'     || '123'
163             'set a complex attribute'                  | 'myUriLdnFirstPart#/attributes/complexAttribute/simpleAttribute' || 'complexAttribute'    || '[simpleAttribute:123]'
164     }
165
166     def 'Build policy executor patch operation details from ProvMnS request parameters with invalid op.'() {
167         given: 'a provMnsRequestParameter and a patchItem list'
168             def path = new RequestPathParameters(uriLdnFirstPart: 'some uri', className: 'some class')
169             def patchItemsList = [new PatchItem(op: 'TEST', 'path':'some uri')]
170         when: 'a build is attempted with an invalid op'
171             objectUnderTest.checkPermissionForEachPatchItem(path, patchItemsList, yangModelCmHandle)
172         then: 'the result is as expected (exception thrown)'
173             thrown(ProvMnSException)
174     }
175
176     def 'Build policy executor create operation details from ProvMnS request parameters where objectClass in resource #scenario.'() {
177         given: 'a provMnsRequestParameter and a resource'
178             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'some uri', className: 'class in uri', id:'my id')
179             def resource = new ResourceOneOf(id: 'some resource id', objectClass: objectInResouce)
180         when: 'a configurationManagementOperation is created and converted to JSON'
181             def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
182         then: 'the result is as expected (using json to compare)'
183             String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"some uri","changeRequest":{"' + changeRequestClassReference + '":[{"id":"my id","attributes":null}]}}'
184             assert jsonObjectMapper.asJsonString(result) == expectedJsonString
185         where:
186             scenario    | objectInResouce     || changeRequestClassReference
187             'populated' | 'class in resource' || 'class in resource'
188             'empty'     | ''                  || 'class in uri'
189             'null'      | null                || 'class in uri'
190     }
191
192     def 'Build Policy Executor Operation Details with a exception during conversion.'() {
193         given: 'a provMnsRequestParameter and a resource'
194             def requestPathParameters = new RequestPathParameters(uriLdnFirstPart: 'some uri', className: 'some class')
195             def resource = new ResourceOneOf(id: 'some resource id')
196         and: 'json object mapper throws an exception'
197             spiedObjectMapper.readValue(*_) >> { throw new JsonProcessingException('original exception message') }
198         when: 'a configurationManagementOperation is created and converted to JSON'
199             objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource)
200         then: 'the expected exception is throw and contains the original message'
201             def thrown = thrown(NcmpException)
202             assert thrown.message.contains('Cannot convert Resource Object')
203             assert thrown.details.contains('original exception message')
204     }
205
206 }