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')
67 def expectedDataspaceNameForModleSync = 'NCMP-Admin'
68 def NO_NAMESPACE = null
70 def expectedDataspaceName = 'NFP-Operational'
71 def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
72 given: 'a cm Handle and a cps path'
73 def cpsPath = '/cps-path'
74 when: 'queryDataNodes is invoked'
75 objectUnderTest.queryDataNodes(cmHandle, cpsPath, fetchDescendantsOption)
76 then: 'the persistence service is called once with the correct parameters'
77 1 * mockCpsQueryService.queryDataNodes(expectedDataspaceName, cmHandle, cpsPath, fetchDescendantsOption)
78 where: 'all fetch descendants options are supported'
79 fetchDescendantsOption << FetchDescendantsOption.values()
82 def 'Create full data node: #scenario.'() {
83 given: 'a cm handle and root xpath'
84 def jsonData = 'some json'
85 when: 'createDataNode is invoked'
86 objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
87 then: 'the CPS service method is invoked once with the expected parameters'
88 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, jsonData, noTimestamp)
89 where: 'following parameters were used'
92 'root level xpath' | '/'
95 def 'Create child data node.'() {
96 given: 'a cm handle and parent node xpath'
97 def jsonData = 'some json'
98 def xpath = '/test-node'
99 when: 'createDataNode is invoked'
100 objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
101 then: 'the CPS service method is invoked once with the expected parameters'
102 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
105 def 'Add list-node elements.'() {
106 given: 'a cm handle and parent node xpath'
107 def jsonData = 'some json'
108 def xpath = '/test-node'
109 when: 'addListNodeElements is invoked'
110 objectUnderTest.addListNodeElements(cmHandle, xpath, jsonData)
111 then: 'the CPS service method is invoked once with the expected parameters'
112 1 * mockCpsDataService.saveListNodeData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
115 def 'Update data node leaves.'() {
116 given: 'a cm Handle and a cps path'
118 def jsonData = 'some json'
119 when: 'updateNodeLeaves is invoked'
120 objectUnderTest.updateNodeLeaves(cmHandle, xpath, jsonData)
121 then: 'the persistence service is called once with the correct parameters'
122 1 * mockCpsDataService.updateNodeLeaves(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
125 def 'Replace data node tree.'() {
126 given: 'a cm Handle and a cps path'
128 def jsonData = 'some json'
129 when: 'replaceNodeTree is invoked'
130 objectUnderTest.replaceNodeTree(cmHandle, xpath, jsonData)
131 then: 'the persistence service is called once with the correct parameters'
132 1 * mockCpsDataService.replaceNodeTree(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
135 def 'Register or re-register a DMI Plugin with #scenario cm handles.'() {
136 given: 'a registration '
137 NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
138 def dmiPluginRegistration = new DmiPluginRegistration()
139 dmiPluginRegistration.dmiPlugin = 'my-server'
140 persistenceCmHandle.cmHandleID = '123'
141 persistenceCmHandle.cmHandleProperties = [name1: 'value1', name2: 'value2']
142 dmiPluginRegistration.createdCmHandles = createdCmHandles
143 dmiPluginRegistration.updatedCmHandles = updatedCmHandles
144 dmiPluginRegistration.removedCmHandles = removedCmHandles
145 def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[{"name":"name1","value":"value1"},{"name":"name2","value":"value2"}]}]}'
146 when: 'registration is updated'
147 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
148 then: 'the CPS save list node data is invoked with the expected parameters'
149 expectedCallsToSaveNode * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
150 '/dmi-registry', expectedJsonData, noTimestamp)
151 and: 'update Node and Child Data Nodes is invoked with correct parameters'
152 expectedCallsToUpdateNode * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves('NCMP-Admin',
153 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData, noTimestamp)
154 and : 'delete list data node is invoked with the correct parameters'
155 expectedCallsToDeleteListDataNode * mockCpsDataService.deleteListNodeData('NCMP-Admin',
156 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
159 scenario | createdCmHandles | updatedCmHandles | removedCmHandles || expectedCallsToSaveNode | expectedCallsToUpdateNode | expectedCallsToDeleteListDataNode
160 'create' | [persistenceCmHandle ] | [] | [] || 1 | 0 | 0
161 'update' | [] | [persistenceCmHandle ] | [] || 0 | 1 | 0
162 'delete' | [] | [] | cmHandlesArray || 0 | 0 | 1
163 'create, update and delete' | [persistenceCmHandle ] | [persistenceCmHandle ] | cmHandlesArray || 1 | 1 | 1
167 def 'Register a DMI Plugin for the given cmHandle without additional properties.'() {
168 given: 'a registration without cmHandle properties '
169 NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
170 def dmiPluginRegistration = new DmiPluginRegistration()
171 dmiPluginRegistration.dmiPlugin = 'my-server'
172 persistenceCmHandle.cmHandleID = '123'
173 persistenceCmHandle.cmHandleProperties = null
174 dmiPluginRegistration.createdCmHandles = [persistenceCmHandle ]
175 def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[]}]}'
176 when: 'registration is updated'
177 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
178 then: 'the CPS save list node data is invoked with the expected parameters'
179 1 * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
180 '/dmi-registry', expectedJsonData, noTimestamp)
183 def 'Get resource data for pass-through operational from dmi.'() {
184 given: 'data node representing cmHandle and its properties'
185 def cmHandleDataNode = getCmHandleDataNodeForTest()
186 when: 'get resource data is called'
187 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
192 then: 'cps data service is being called once to get data node'
193 1 * mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
194 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
195 and: 'dmi operation is being called to get resource data'
196 1 * mockDmiOperations.getResourceDataOperationalFromDmi('testDmiService',
202 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >>
203 new ResponseEntity<>('result-json', HttpStatus.OK)
204 and: 'dmi returns ok response'
205 response == 'result-json'
208 def 'Get resource data for pass-through operational from dmi threw parsing exception.'() {
209 given: 'data node representing cmHandle and its properties'
210 def cmHandleDataNode = getCmHandleDataNodeForTest()
211 and: 'cps data service returns valid cmHandle data node'
212 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
213 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
214 and: 'objectMapper not able to parse object'
215 def mockObjectMapper = Mock(ObjectMapper)
216 objectUnderTest.objectMapper = mockObjectMapper
217 mockObjectMapper.writeValueAsString(_) >> { throw new JsonProcessingException("testException") }
218 when: 'get resource data is called'
219 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
224 then: 'exception is thrown'
225 thrown(NcmpException.class)
228 def 'Get resource data for pass-through operational from dmi return NOK response.'() {
229 given: 'data node representing cmHandle and its properties'
230 def cmHandleDataNode = getCmHandleDataNodeForTest()
231 and: 'cps data service returns valid cmHandle data node'
232 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
233 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
234 and: 'dmi returns NOK response'
235 mockDmiOperations.getResourceDataOperationalFromDmi('testDmiService',
241 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}')
242 >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
243 when: 'get resource data is called'
244 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
249 then: 'exception is thrown'
250 thrown(NcmpException.class)
253 def 'Get resource data for pass-through running from dmi.'() {
254 given: 'data node representing cmHandle and its properties'
255 def cmHandleDataNode = getCmHandleDataNodeForTest()
256 and: 'cpsDataService returns valid dataNode'
257 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
258 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
259 and: 'dmi returns valid response and data'
260 mockDmiOperations.getResourceDataPassThroughRunningFromDmi('testDmiService',
266 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >> new ResponseEntity<>('{result-json}', HttpStatus.OK)
267 when: 'get resource data is called'
268 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
273 then: 'get resource data returns expected response'
274 response == '{result-json}'
277 def 'Get resource data for pass-through running from dmi threw parsing exception.'() {
278 given: 'data node representing cmHandle and its properties'
279 def cmHandleDataNode = getCmHandleDataNodeForTest()
280 and: 'cpsDataService returns valid dataNode'
281 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
282 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
283 and: 'objectMapper not able to parse object'
284 def mockObjectMapper = Mock(ObjectMapper)
285 objectUnderTest.objectMapper = mockObjectMapper
286 mockObjectMapper.writeValueAsString(_) >> { throw new JsonProcessingException("testException") }
287 when: 'get resource data is called'
288 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
293 then: 'exception is thrown'
294 thrown(NcmpException.class)
297 def 'Get resource data for pass-through running from dmi return NOK response.'() {
298 given: 'data node representing cmHandle and its properties'
299 def cmHandleDataNode = getCmHandleDataNodeForTest()
300 and: 'cpsDataService returns valid dataNode'
301 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
302 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
303 and: 'dmi returns NOK response'
304 mockDmiOperations.getResourceDataPassThroughRunningFromDmi('testDmiService',
310 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}')
311 >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
312 when: 'get resource data is called'
313 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
318 then: 'exception is thrown'
319 thrown(NcmpException.class)
322 def 'Write resource data for pass-through running from dmi using POST.'() {
323 given: 'data node representing cmHandle and its properties'
324 def cmHandleDataNode = getCmHandleDataNodeForTest()
325 and: 'cpsDataService returns valid dataNode'
326 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
327 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
328 when: 'get resource data is called'
329 objectUnderTest.createResourceDataPassThroughRunningForCmHandle('testCmHandle',
331 '{some-json}', 'application/json')
332 then: 'dmi called with correct data'
333 1 * mockDmiOperations.createResourceDataPassThroughRunningFromDmi('testDmiService',
336 '{"operation":"create","dataType":"application/json","data":"{some-json}","cmHandleProperties":{"testName":"testValue"}}')
337 >> { new ResponseEntity<>(HttpStatus.CREATED) }
340 def 'Write resource data for pass-through running from dmi using POST "not found" response (from DMI).'() {
341 given: 'data node representing cmHandle and its properties'
342 def cmHandleDataNode = getCmHandleDataNodeForTest()
343 and: 'cpsDataService returns valid dataNode'
344 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
345 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
346 and: 'dmi throws exception'
347 1 * mockDmiOperations.createResourceDataPassThroughRunningFromDmi(_ as String, _ as String, _ as String, _ as String)
348 >> { new ResponseEntity<>(HttpStatus.NOT_FOUND) }
349 when: 'get resource data is called'
350 objectUnderTest.createResourceDataPassThroughRunningForCmHandle('testCmHandle',
352 '{some-json}', 'application/json')
353 then: 'exception is thrown'
354 thrown(NcmpException.class)
357 def 'Sync model for a (new) cm handle with #scenario'() {
358 given: 'DMI PLug-in returns a list of module references'
359 getModulesForCmHandle()
360 def knownModule1 = new ModuleReference('module1', NO_NAMESPACE, '1')
361 def knownOtherModule = new ModuleReference('some other module', NO_NAMESPACE, 'some revision')
362 and: 'CPS-Core returns list of known modules'
363 mockCpsModuleService.getAllYangResourcesModuleReferences() >> [knownModule1, knownOtherModule]
364 and: 'DMI-Plugin returns resource(s) for "new" module(s)'
365 def moduleResources = new ResponseEntity<String>(sdncReponseBody, HttpStatus.OK)
366 mockDmiOperations.getResourceFromDmi(_, cmHandleForModelSync.getId(), 'moduleResources') >> moduleResources
367 when: 'module Sync is triggered'
368 objectUnderTest.createAnchorAndSyncModel(cmHandleForModelSync)
369 then: 'the CPS module service is called once with the correct parameters'
370 1 * mockCpsModuleService.createSchemaSetFromModules(expectedDataspaceNameForModleSync, cmHandleForModelSync.getId(), expectedYangResourceToContentMap , [knownModule1])
371 and: 'admin service create anchor method has been called with correct parameters'
372 1 * mockCpsAdminService.createAnchor(expectedDataspaceNameForModleSync, cmHandleForModelSync.getId(), cmHandleForModelSync.getId())
373 where: 'the following responses are recieved from SDNC'
374 scenario | sdncReponseBody || expectedYangResourceToContentMap
375 'one unknown module' | '[{"moduleName" : "someModule", "revision" : "1","yangSource": "someResource"}]' || [someModule: 'someResource']
376 'no unknown module' | '[]' || [:]
379 def getModulesForCmHandle() {
380 def jsonData = TestUtils.getResourceFileContent('cmHandleModules.json')
381 mockDmiProperties.getAuthUsername() >> 'someUser'
382 mockDmiProperties.getAuthPassword() >> 'somePassword'
383 mockDmiProperties.getDmiPluginBasePath() >> 'someUrl'
384 def moduleReferencesFromCmHandleAsJson = new ResponseEntity<String>(jsonData, HttpStatus.OK)
385 mockDmiOperations.getResourceFromDmi(_, cmHandleForModelSync.getId(), 'modules') >> moduleReferencesFromCmHandleAsJson
388 def getObjectUnderTestWithModelSyncDisabled() {
389 def objectUnderTest = Spy(new NetworkCmProxyDataServiceImpl(mockDmiOperations, mockCpsModuleService,
390 mockCpsDataService, mockCpsQueryService, mockCpsAdminService, new ObjectMapper()))
391 objectUnderTest.createAnchorAndSyncModel(_) >> null
392 return objectUnderTest
395 def getCmHandleDataNodeForTest() {
396 def cmHandleDataNode = new DataNode()
397 cmHandleDataNode.leaves = ['dmi-service-name': 'testDmiService']
398 def cmHandlePropertyDataNode = new DataNode()
399 cmHandlePropertyDataNode.leaves = ['name': 'testName', 'value': 'testValue']
400 cmHandleDataNode.childDataNodes = [cmHandlePropertyDataNode]
401 return cmHandleDataNode