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.ncmp.api.impl
25 import com.fasterxml.jackson.core.JsonProcessingException
26 import com.fasterxml.jackson.databind.ObjectMapper
27 import org.onap.cps.api.CpsAdminService
28 import org.onap.cps.api.CpsDataService
29 import org.onap.cps.api.CpsModuleService
30 import org.onap.cps.api.CpsQueryService
31 import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration
32 import org.onap.cps.ncmp.api.impl.exception.NcmpException
33 import org.onap.cps.ncmp.api.impl.operation.DmiOperations
34 import org.onap.cps.ncmp.api.models.CmHandle
35 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
36 import org.onap.cps.ncmp.api.models.PersistenceCmHandle
37 import org.onap.cps.ncmp.utils.TestUtils
38 import org.onap.cps.spi.FetchDescendantsOption
39 import org.onap.cps.spi.model.DataNode
40 import org.onap.cps.spi.model.ModuleReference
41 import org.springframework.http.HttpStatus
42 import org.springframework.http.ResponseEntity
43 import spock.lang.Shared
44 import spock.lang.Specification
46 class NetworkCmProxyDataServiceImplSpec extends Specification {
49 def persistenceCmHandle = new CmHandle()
51 def cmHandlesArray = ['cmHandle001']
53 def mockCpsDataService = Mock(CpsDataService)
54 def mockCpsQueryService = Mock(CpsQueryService)
55 def mockDmiOperations = Mock(DmiOperations)
56 def mockCpsModuleService = Mock(CpsModuleService)
57 def mockCpsAdminService = Mock(CpsAdminService)
58 def mockDmiProperties = Mock(NcmpConfiguration.DmiProperties)
60 def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockDmiOperations, mockCpsModuleService,
61 mockCpsDataService, mockCpsQueryService, mockCpsAdminService, new ObjectMapper())
63 def cmHandle = 'some handle'
64 def noTimestamp = null
65 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
66 def cmHandleForModelSync = new PersistenceCmHandle(id:'some cm handle', dmiServiceName: 'some service name')
68 def expectedDataspaceName = 'NFP-Operational'
69 def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
70 given: 'a cm Handle and a cps path'
71 def cpsPath = '/cps-path'
72 when: 'queryDataNodes is invoked'
73 objectUnderTest.queryDataNodes(cmHandle, cpsPath, fetchDescendantsOption)
74 then: 'the persistence service is called once with the correct parameters'
75 1 * mockCpsQueryService.queryDataNodes(expectedDataspaceName, cmHandle, cpsPath, fetchDescendantsOption)
76 where: 'all fetch descendants options are supported'
77 fetchDescendantsOption << FetchDescendantsOption.values()
80 def 'Create full data node: #scenario.'() {
81 given: 'a cm handle and root xpath'
82 def jsonData = 'some json'
83 when: 'createDataNode is invoked'
84 objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
85 then: 'the CPS service method is invoked once with the expected parameters'
86 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, jsonData, noTimestamp)
87 where: 'following parameters were used'
90 'root level xpath' | '/'
93 def 'Create child data node.'() {
94 given: 'a cm handle and parent node xpath'
95 def jsonData = 'some json'
96 def xpath = '/test-node'
97 when: 'createDataNode is invoked'
98 objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
99 then: 'the CPS service method is invoked once with the expected parameters'
100 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
103 def 'Add list-node elements.'() {
104 given: 'a cm handle and parent node xpath'
105 def jsonData = 'some json'
106 def xpath = '/test-node'
107 when: 'addListNodeElements is invoked'
108 objectUnderTest.addListNodeElements(cmHandle, xpath, jsonData)
109 then: 'the CPS service method is invoked once with the expected parameters'
110 1 * mockCpsDataService.saveListNodeData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
113 def 'Update data node leaves.'() {
114 given: 'a cm Handle and a cps path'
116 def jsonData = 'some json'
117 when: 'updateNodeLeaves is invoked'
118 objectUnderTest.updateNodeLeaves(cmHandle, xpath, jsonData)
119 then: 'the persistence service is called once with the correct parameters'
120 1 * mockCpsDataService.updateNodeLeaves(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
123 def 'Replace data node tree.'() {
124 given: 'a cm Handle and a cps path'
126 def jsonData = 'some json'
127 when: 'replaceNodeTree is invoked'
128 objectUnderTest.replaceNodeTree(cmHandle, xpath, jsonData)
129 then: 'the persistence service is called once with the correct parameters'
130 1 * mockCpsDataService.replaceNodeTree(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
133 def 'Register or re-register a DMI Plugin with #scenario cm handles.'() {
134 given: 'a registration '
135 NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
136 def dmiPluginRegistration = new DmiPluginRegistration()
137 dmiPluginRegistration.dmiPlugin = 'my-server'
138 persistenceCmHandle.cmHandleID = '123'
139 persistenceCmHandle.cmHandleProperties = [name1: 'value1', name2: 'value2']
140 dmiPluginRegistration.createdCmHandles = createdCmHandles
141 dmiPluginRegistration.updatedCmHandles = updatedCmHandles
142 dmiPluginRegistration.removedCmHandles = removedCmHandles
143 def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[{"name":"name1","value":"value1"},{"name":"name2","value":"value2"}]}]}'
144 when: 'registration is updated'
145 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
146 then: 'the CPS save list node data is invoked with the expected parameters'
147 expectedCallsToSaveNode * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
148 '/dmi-registry', expectedJsonData, noTimestamp)
149 and: 'update Node and Child Data Nodes is invoked with correct parameters'
150 expectedCallsToUpdateNode * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves('NCMP-Admin',
151 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData, noTimestamp)
152 and : 'delete list data node is invoked with the correct parameters'
153 expectedCallsToDeleteListDataNode * mockCpsDataService.deleteListNodeData('NCMP-Admin',
154 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
157 scenario | createdCmHandles | updatedCmHandles | removedCmHandles || expectedCallsToSaveNode | expectedCallsToUpdateNode | expectedCallsToDeleteListDataNode
158 'create' | [persistenceCmHandle ] | [] | [] || 1 | 0 | 0
159 'update' | [] | [persistenceCmHandle ] | [] || 0 | 1 | 0
160 'delete' | [] | [] | cmHandlesArray || 0 | 0 | 1
161 'create, update and delete' | [persistenceCmHandle ] | [persistenceCmHandle ] | cmHandlesArray || 1 | 1 | 1
165 def 'Register a DMI Plugin for the given cmHandle without additional properties.'() {
166 given: 'a registration without cmHandle properties '
167 NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
168 def dmiPluginRegistration = new DmiPluginRegistration()
169 dmiPluginRegistration.dmiPlugin = 'my-server'
170 persistenceCmHandle.cmHandleID = '123'
171 persistenceCmHandle.cmHandleProperties = null
172 dmiPluginRegistration.createdCmHandles = [persistenceCmHandle ]
173 def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[]}]}'
174 when: 'registration is updated'
175 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
176 then: 'the CPS save list node data is invoked with the expected parameters'
177 1 * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
178 '/dmi-registry', expectedJsonData, noTimestamp)
181 def 'Get resource data for pass-through operational from dmi.'() {
182 given: 'data node representing cmHandle and its properties'
183 def cmHandleDataNode = getCmHandleDataNodeForTest()
184 and: 'data node is got from data service'
185 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
186 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
187 and: 'resource data is got from DMI'
188 mockDmiOperations.getResourceDataOperationalFromDmi('testDmiService',
194 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >> new ResponseEntity<>('result-json', HttpStatus.OK)
195 when: 'get resource data is called'
196 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
201 then: 'dmi returns ok response'
202 response == 'result-json'
205 def 'Get resource data for pass-through operational from dmi threw parsing exception.'() {
206 given: 'data node representing cmHandle and its properties'
207 def cmHandleDataNode = getCmHandleDataNodeForTest()
208 and: 'cps data service returns valid cmHandle data node'
209 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
210 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
211 and: 'objectMapper not able to parse object'
212 def mockObjectMapper = Mock(ObjectMapper)
213 objectUnderTest.objectMapper = mockObjectMapper
214 mockObjectMapper.writeValueAsString(_) >> { throw new JsonProcessingException("testException") }
215 when: 'get resource data is called'
216 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
221 then: 'exception is thrown'
222 thrown(NcmpException.class)
225 def 'Get resource data for pass-through operational from dmi return NOK response.'() {
226 given: 'data node representing cmHandle and its properties'
227 def cmHandleDataNode = getCmHandleDataNodeForTest()
228 and: 'cps data service returns valid cmHandle data node'
229 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
230 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
231 and: 'dmi returns NOK response'
232 mockDmiOperations.getResourceDataOperationalFromDmi('testDmiService',
238 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}')
239 >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
240 when: 'get resource data is called'
241 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
246 then: 'exception is thrown'
247 thrown(NcmpException.class)
250 def 'Get resource data for pass-through running from dmi.'() {
251 given: 'data node representing cmHandle and its properties'
252 def cmHandleDataNode = getCmHandleDataNodeForTest()
253 and: 'cpsDataService returns valid dataNode'
254 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
255 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
256 and: 'dmi returns valid response and data'
257 mockDmiOperations.getResourceDataPassThroughRunningFromDmi('testDmiService',
263 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >> new ResponseEntity<>('{result-json}', HttpStatus.OK)
264 when: 'get resource data is called'
265 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
270 then: 'get resource data returns expected response'
271 response == '{result-json}'
274 def 'Get resource data for pass-through running from dmi threw parsing exception.'() {
275 given: 'data node representing cmHandle and its properties'
276 def cmHandleDataNode = getCmHandleDataNodeForTest()
277 and: 'cpsDataService returns valid dataNode'
278 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
279 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
280 and: 'objectMapper not able to parse object'
281 def mockObjectMapper = Mock(ObjectMapper)
282 objectUnderTest.objectMapper = mockObjectMapper
283 mockObjectMapper.writeValueAsString(_) >> { throw new JsonProcessingException("testException") }
284 when: 'get resource data is called'
285 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
290 then: 'exception is thrown'
291 thrown(NcmpException.class)
294 def 'Get resource data for pass-through running from dmi return NOK response.'() {
295 given: 'data node representing cmHandle and its properties'
296 def cmHandleDataNode = getCmHandleDataNodeForTest()
297 and: 'cpsDataService returns valid dataNode'
298 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
299 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
300 and: 'dmi returns NOK response'
301 mockDmiOperations.getResourceDataPassThroughRunningFromDmi('testDmiService',
307 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}')
308 >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
309 when: 'get resource data is called'
310 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
315 then: 'exception is thrown'
316 thrown(NcmpException.class)
319 def 'Write resource data for pass-through running from dmi using POST.'() {
320 given: 'data node representing cmHandle and its properties'
321 def cmHandleDataNode = getCmHandleDataNodeForTest()
322 and: 'cpsDataService returns valid cm-handle datanode'
323 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
324 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
325 when: 'get resource data is called'
326 objectUnderTest.createResourceDataPassThroughRunningForCmHandle('testCmHandle',
328 '{some-json}', 'application/json')
329 then: 'dmi called with correct data'
330 1 * mockDmiOperations.createResourceDataPassThroughRunningFromDmi('testDmiService',
333 '{"operation":"create","dataType":"application/json","data":"{some-json}","cmHandleProperties":{"testName":"testValue"}}')
334 >> { new ResponseEntity<>(HttpStatus.OK) }
337 def 'Write resource data for pass-through running from dmi using POST "not found" response (from DMI).'() {
338 given: 'data node representing cmHandle and its properties'
339 def cmHandleDataNode = getCmHandleDataNodeForTest()
340 and: 'cpsDataService returns valid dataNode'
341 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
342 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
343 and: 'dmi throws exception'
344 mockDmiOperations.createResourceDataPassThroughRunningFromDmi(_ as String, _ as String, _ as String, _ as String)
345 >> { new ResponseEntity<>(HttpStatus.NOT_FOUND) }
346 when: 'get resource data is called'
347 objectUnderTest.createResourceDataPassThroughRunningForCmHandle('testCmHandle',
349 '{some-json}', 'application/json')
350 then: 'exception is thrown'
351 thrown(NcmpException.class)
354 def 'Sync model for a (new) cm handle with #scenario'() {
355 given: 'DMI PLug-in returns a list of module references'
356 getModulesForCmHandle()
357 def knownModule1 = new ModuleReference('module1', '1')
358 def knownOtherModule = new ModuleReference('some other module', 'some revision')
359 and: 'CPS-Core returns list of known modules'
360 mockCpsModuleService.getYangResourceModuleReferences(_) >> [knownModule1, knownOtherModule]
361 and: 'DMI-Plugin returns resource(s) for "new" module(s)'
362 def moduleResources = new ResponseEntity<String>(sdncReponseBody, HttpStatus.OK)
363 mockDmiOperations.getResourceFromDmiWithJsonData(_, _, _, 'moduleResources') >> moduleResources
364 when: 'module Sync is triggered'
365 objectUnderTest.createAnchorAndSyncModel(cmHandleForModelSync)
366 then: 'the CPS module service is called once with the correct parameters'
367 1 * mockCpsModuleService.createSchemaSetFromModules(expectedDataspaceName, cmHandleForModelSync.getId(), expectedYangResourceToContentMap, [knownModule1])
368 and: 'admin service create anchor method has been called with correct parameters'
369 1 * mockCpsAdminService.createAnchor(expectedDataspaceName, cmHandleForModelSync.getId(), cmHandleForModelSync.getId())
370 where: 'the following responses are recieved from SDNC'
371 scenario | sdncReponseBody || expectedYangResourceToContentMap
372 'one unknown module' | '[{"moduleName" : "someModule", "revision" : "1","yangSource": "[some yang source]"}]' || [someModule: 'some yang source']
373 'no unknown module' | '[]' || [:]
376 def getModulesForCmHandle() {
377 def jsonData = TestUtils.getResourceFileContent('cmHandleModules.json')
378 mockDmiProperties.getAuthUsername() >> 'someUser'
379 mockDmiProperties.getAuthPassword() >> 'somePassword'
380 mockDmiProperties.getDmiPluginBasePath() >> 'someUrl'
381 def moduleReferencesFromCmHandleAsJson = new ResponseEntity<String>(jsonData, HttpStatus.OK)
382 mockDmiOperations.getResourceFromDmi(_, cmHandleForModelSync.getId(), 'modules') >> moduleReferencesFromCmHandleAsJson
385 def getObjectUnderTestWithModelSyncDisabled() {
386 def objectUnderTest = Spy(new NetworkCmProxyDataServiceImpl(mockDmiOperations, mockCpsModuleService,
387 mockCpsDataService, mockCpsQueryService, mockCpsAdminService, new ObjectMapper()))
388 objectUnderTest.createAnchorAndSyncModel(_) >> null
389 return objectUnderTest
392 def getCmHandleDataNodeForTest() {
393 def cmHandleDataNode = new DataNode()
394 cmHandleDataNode.leaves = ['dmi-service-name': 'testDmiService']
395 def cmHandlePropertyDataNode = new DataNode()
396 cmHandlePropertyDataNode.leaves = ['name': 'testName', 'value': 'testValue']
397 cmHandleDataNode.childDataNodes = [cmHandlePropertyDataNode]
398 return cmHandleDataNode