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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.cps.ncmp.impl.data.policyexecutor
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;
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;
38 class OperationDetailsFactorySpec extends Specification {
40 def spiedObjectMapper = Spy(ObjectMapper)
41 def jsonObjectMapper = new JsonObjectMapper(spiedObjectMapper)
42 def policyExecutor = Mock(PolicyExecutor)
44 OperationDetailsFactory objectUnderTest = new OperationDetailsFactory(jsonObjectMapper, spiedObjectMapper, policyExecutor)
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')
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'
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
72 scenario | classNameInBody || expectedChangeRequestKey
73 'populated' | 'class in body' || 'class in body'
74 'empty' | '' || 'class in uri'
75 'null' | null || 'class in uri'
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'
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
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, _, _, _)
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')
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, _, _, _)
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(
142 requestPathParameters.toAlternateId(),
143 { json -> assert json.contains(attributesValueInOperation) } // check for more details eg. verify type
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"]'
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]'
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)
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
186 scenario | objectInResouce || changeRequestClassReference
187 'populated' | 'class in resource' || 'class in resource'
188 'empty' | '' || 'class in uri'
189 'null' | null || 'class in uri'
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')