From 6229cfeafade160ed281fc410454c7498b8a21dc Mon Sep 17 00:00:00 2001 From: ToineSiebelink Date: Tue, 16 Jan 2024 08:40:51 +0000 Subject: Clear instance based Schema Context Cache upon validation errors - retry yang parser exceptions one time by clearing cache - split yangUtils into YangParserHelper (instance based) and YangUtils for non-parsering static methods - removed public methods only used from test. Refactored to use variation with (empty) parent path - removed use of optional for parentPath, easier to handle with just an empty string! - make methods no longer used in production code in YangUtils are private - udpate testware to use proper public methods instead of private methods of yang utils / parser (helper) Issue-ID:CPS-2000 Signed-off-by: ToineSiebelink Change-Id: I0c7590a5e1495d047006e7136f1bd873be37f7b0 --- .../cps/api/impl/CpsDataServiceImplSpec.groovy | 10 +- .../onap/cps/api/impl/E2ENetworkSliceSpec.groovy | 15 +- .../onap/cps/spi/model/DataNodeBuilderSpec.groovy | 18 ++- .../test/groovy/org/onap/cps/utils/GsonSpec.groovy | 28 +++- .../org/onap/cps/utils/XmlFileUtilsSpec.groovy | 4 +- .../org/onap/cps/utils/YangParserHelperSpec.groovy | 166 +++++++++++++++++++++ .../org/onap/cps/utils/YangParserSpec.groovy | 85 +++++++++++ .../groovy/org/onap/cps/utils/YangUtilsSpec.groovy | 138 +---------------- 8 files changed, 298 insertions(+), 166 deletions(-) create mode 100644 cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy create mode 100644 cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy (limited to 'cps-service/src/test') diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index 322d2c9152..b2b2d7d44c 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -34,28 +34,26 @@ import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.exceptions.SessionManagerException import org.onap.cps.spi.exceptions.SessionTimeoutException import org.onap.cps.spi.model.Anchor -import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.DataNodeBuilder import org.onap.cps.spi.utils.CpsValidator import org.onap.cps.utils.ContentType -import org.onap.cps.utils.TimedYangParser +import org.onap.cps.utils.YangParser +import org.onap.cps.utils.YangParserHelper import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Shared import spock.lang.Specification import java.time.OffsetDateTime -import java.util.stream.Collectors class CpsDataServiceImplSpec extends Specification { def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService) def mockCpsAnchorService = Mock(CpsAnchorService) def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockCpsValidator = Mock(CpsValidator) - def timedYangParser = new TimedYangParser() + def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache) def mockCpsDeltaService = Mock(CpsDeltaService); - def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService, - mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService) + def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService) def setup() { mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy index 4782468f19..140dfaac96 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation. + * Copyright (C) 2021-2024 Nordix Foundation. * Modifications Copyright (C) 2021-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. @@ -27,12 +27,12 @@ import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDeltaService import org.onap.cps.spi.CpsDataPersistenceService -import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.CpsModulePersistenceService import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.utils.CpsValidator -import org.onap.cps.utils.TimedYangParser -import org.onap.cps.utils.YangUtils +import org.onap.cps.utils.ContentType +import org.onap.cps.utils.YangParser +import org.onap.cps.utils.YangParserHelper import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Specification @@ -44,14 +44,13 @@ class E2ENetworkSliceSpec extends Specification { def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockCpsValidator = Mock(CpsValidator) def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder() - def timedYangParser = new TimedYangParser() + def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache) def mockCpsDeltaService = Mock(CpsDeltaService) def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) - def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockCpsAnchorService, - mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService) + def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService) def dataspaceName = 'someDataspace' def anchorName = 'someAnchor' @@ -165,6 +164,6 @@ class E2ENetworkSliceSpec extends Specification { expect: 'schema context is built with no exception indicating the schema set being valid ' def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap).getSchemaContext() and: 'data is parsed with no exception indicating the model match' - YangUtils.parseJsonData(jsonData, schemaContext) != null + new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '') != null } } diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy index fcbae628e6..e305abee86 100644 --- a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2021-2023 Nordix Foundation. + * Modifications Copyright (C) 2021-2024 Nordix Foundation. * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,8 +23,9 @@ package org.onap.cps.spi.model import org.onap.cps.TestUtils import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.utils.ContentType import org.onap.cps.utils.DataMapUtils -import org.onap.cps.utils.YangUtils +import org.onap.cps.utils.YangParserHelper import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode import org.opendaylight.yangtools.yang.data.api.schema.ForeignDataNode @@ -33,6 +34,7 @@ import spock.lang.Specification class DataNodeBuilderSpec extends Specification { def objectUnderTest = new DataNodeBuilder() + def yangParserHelper = new YangParserHelper() def expectedLeavesByXpathMap = [ '/test-tree' : [], @@ -58,7 +60,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data parsed into container node object' def jsonData = TestUtils.getResourceFileContent('test-tree.json') - def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -78,7 +80,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data parsed into container node object' def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }' - def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree") + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree') when: 'the container node is converted to a data node with parent node xpath defined' def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath('/test-tree').build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -94,7 +96,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data parsed into container node object' def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json') - def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') when: 'the container node is converted to a data node ' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -127,7 +129,7 @@ class DataNodeBuilderSpec extends Specification { def parentNodeXpath = "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']" and: 'the json data fragment parsed into container node object for given parent node xpath' def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}' - def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath) when: 'the container node is converted to a data node with given parent node xpath' def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).build() then: 'the resulting data node represents a child of augmentation node' @@ -142,7 +144,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data fragment parsed into container node object' def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json') - def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -160,7 +162,7 @@ class DataNodeBuilderSpec extends Specification { and: 'parent node xpath referencing parent of list element' def parentNodeXpath = '/test-tree' and: 'the json data fragment (list element) parsed into container node object' - def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath) when: 'the container node is converted to a data node collection' def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).buildCollection() def resultXpaths = result.collect { it.getXpath() } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/GsonSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/GsonSpec.groovy index c100ea31d5..7e211deb78 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/GsonSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/GsonSpec.groovy @@ -1,13 +1,32 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + package org.onap.cps.utils import com.google.gson.stream.JsonReader import org.onap.cps.TestUtils import spock.lang.Specification +class GsonSpec extends Specification { -class GsonSpec extends Specification{ - - def 'Iterate over JSON data with gson JsonReader'(){ + def 'Iterate over JSON data with gson JsonReader'() { given: 'json data with two objects and JSON reader' def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json') def objectUnderTest = new JsonReader(new StringReader(jsonData)); @@ -17,7 +36,7 @@ class GsonSpec extends Specification{ noExceptionThrown() } - def iterateWithJsonReader(JsonReader jsonReader){ + def iterateWithJsonReader(JsonReader jsonReader) { switch(jsonReader.peek()) { case "STRING": print(jsonReader.nextString() + " ") @@ -36,5 +55,4 @@ class GsonSpec extends Specification{ } } - } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy index 3864a5253a..dc6027de25 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG - * Modifications Copyright (c) 2023 Nordix Foundation + * Modifications Copyright (c) 2023-2024 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ class XmlFileUtilsSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() and: 'Parent schema node by xPath' - def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext).get("dataSchemaNode") + def parentSchemaNode = YangParserHelper.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext).get('dataSchemaNode') when: 'the XML data is parsed' def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath) then: 'the result XML is wrapped by xPath defined parent root node' diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy new file mode 100644 index 0000000000..073383113d --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy @@ -0,0 +1,166 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils + +import org.onap.cps.TestUtils +import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.opendaylight.yangtools.yang.common.QName +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode +import spock.lang.Specification + +class YangParserHelperSpec extends Specification { + + def objectUnderTest = new YangParserHelper() + + def 'Parsing a valid multicontainer Json String.'() { + given: 'a yang model (file)' + def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json') + and: 'a model for that data' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('multipleDataTree.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'the json data is parsed' + def result = objectUnderTest.parseData(ContentType.JSON, jsonData, schemaContext, '') + then: 'a ContainerNode holding collection of normalized nodes is returned' + result.body().getAt(index) instanceof NormalizedNode == true + then: 'qualified name of children created is as expected' + result.body().getAt(index).getIdentifier().nodeType == QName.create('org:onap:ccsdk:multiDataTree', '2020-09-15', nodeName) + where: + index | nodeName + 0 | 'first-container' + 1 | 'last-container' + } + + def 'Parsing a valid #scenario String.'() { + given: 'a yang model (file)' + def fileData = TestUtils.getResourceFileContent(contentFile) + and: 'a model for that data' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'the data is parsed' + NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '') + then: 'the result is a normalized node of the correct type' + if (revision) { + result.identifier.nodeType == QName.create(namespace, revision, localName) + } else { + result.identifier.nodeType == QName.create(namespace, localName) + } + where: + scenario | contentFile | contentType | namespace | revision | localName + 'JSON' | 'bookstore.json' | ContentType.JSON | 'org:onap:ccsdk:sample' | '2020-09-15' | 'bookstore' + 'XML' | 'bookstore.xml' | ContentType.XML | 'urn:ietf:params:xml:ns:netconf:base:1.0' | '' | 'bookstore' + } + + def 'Parsing invalid data: #description.'() { + given: 'a yang model (file)' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'invalid data is parsed' + objectUnderTest.parseData(contentType, invalidData, schemaContext, '') + then: 'an exception is thrown' + thrown(DataValidationException) + where: 'the following invalid data is provided' + invalidData | contentType | description + '{incomplete json' | ContentType.JSON | 'incomplete json' + '{"test:bookstore": {"address": "Parnell st." }}' | ContentType.JSON | 'json with un-modelled data' + '{" }' | ContentType.JSON | 'json with syntax exception' + '' | ContentType.XML | 'incomplete xml' + 'blabla' | ContentType.XML | 'xml with invalid model' + '' | ContentType.XML | 'empty xml' + } + + def 'Parsing data fragment by xpath for #scenario.'() { + given: 'schema context' + def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() + when: 'json string is parsed' + def result = objectUnderTest.parseData(contentType, nodeData, schemaContext, parentNodeXpath) + then: 'a ContainerNode holding collection of normalized nodes is returned' + result.body().getAt(0) instanceof NormalizedNode == true + then: 'result represents a node of expected type' + result.body().getAt(0).getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName) + where: + scenario | contentType | nodeData | parentNodeXpath || nodeName + 'JSON list element as container' | ContentType.JSON | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch' + 'JSON list element within list' | ContentType.JSON | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch' + 'JSON container element' | ContentType.JSON | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest' + 'XML element test tree' | ContentType.XML | 'LeftSmallSparrow' | '/test-tree' || 'branch' + 'XML element branch xpath' | ContentType.XML | 'LeftSmallSparrowRobin' | '/test-tree' || 'branch' + 'XML container element' | ContentType.XML | 'SmallSparrow' | '/test-tree/branch[@name=\'Branch\']' || 'nest' + } + + def 'Parsing json data fragment by xpath error scenario: #scenario.'() { + given: 'schema context' + def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() + when: 'json string is parsed' + objectUnderTest.parseData(ContentType.JSON, '{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, parentNodeXpath) + then: 'expected exception is thrown' + thrown(DataValidationException) + where: + scenario | parentNodeXpath + 'xpath has no identifiers' | '/' + 'xpath has no valid identifiers' | '/[@name=\'Name\']' + 'invalid parent path' | '/test-bush' + 'another invalid parent path' | '/test-tree/branch[@name=\'Branch\']/nest/name/last-name' + 'fragment does not belong to parent' | '/test-tree/' + } + + def 'Parsing json data with invalid json string: #description.'() { + given: 'schema context' + def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() + when: 'malformed json string is parsed' + objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '') + then: 'an exception is thrown' + thrown(DataValidationException) + where: 'the following malformed json is provided' + description | invalidJson + 'malformed json string with unterminated array data' | '{bookstore={categories=[{books=[{authors=[Iain M. Banks]}]}]}}' + 'incorrect json' | '{" }' + } + + def 'Parsing json data with space.'() { + given: 'schema context' + def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() + and: 'some json data with space in the array elements' + def jsonDataWithSpacesInArrayElement = TestUtils.getResourceFileContent('bookstore.json') + when: 'that json data is parsed' + objectUnderTest.parseData(ContentType.JSON, jsonDataWithSpacesInArrayElement, schemaContext, '') + then: 'no exception thrown' + noExceptionThrown() + } + + def 'Converting xPath to nodeId for #scenario.'() { + when: 'xPath is parsed' + def result = objectUnderTest.xpathToNodeIdSequence(xPath) + then: 'result represents an array of expected identifiers' + assert result == expectedNodeIdentifier + where: 'the following parameters are used' + scenario | xPath || expectedNodeIdentifier + 'container xpath' | '/test-tree' || ['test-tree'] + 'xpath contains list attribute' | '/test-tree/branch[@name=\'Branch\']' || ['test-tree','branch'] + 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] + } + + +} diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy new file mode 100644 index 0000000000..99070fe729 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy @@ -0,0 +1,85 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils + +import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.spi.model.Anchor +import org.onap.cps.yang.YangTextSchemaSourceSet +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode +import org.opendaylight.yangtools.yang.model.api.SchemaContext +import spock.lang.Specification +import org.onap.cps.api.impl.YangTextSchemaSourceSetCache + +class YangParserSpec extends Specification { + + def mockYangParserHelper = Mock(YangParserHelper) + def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) + + def objectUnderTest = new YangParser(mockYangParserHelper, mockYangTextSchemaSourceSetCache) + + def anchor = new Anchor(dataspaceName: 'my dataspace', schemaSetName: 'my schema') + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + def mockSchemaContext = Mock(SchemaContext) + def containerNodeFromYangUtils = Mock(ContainerNode) + + def noParent = '' + + def setup() { + mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet + mockYangTextSchemaSourceSet.getSchemaContext() >> mockSchemaContext + } + + def 'Parsing data.'() { + given: 'the yang parser (utility) always returns a container node' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils + when: 'parsing some json data' + def result = objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent) + then: 'the schema source set for the correct dataspace and schema set is retrieved form the cache' + 1 * mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet + and: 'the result is the same container node as return from yang utils' + assert result == containerNodeFromYangUtils + and: 'nothing is removed from the cache' + 0 * mockYangTextSchemaSourceSetCache.removeFromCache(*_) + } + + def 'Parsing data with exception on first attempt.'() { + given: 'the yang parser throws an exception on the first attempt only' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> { throw new DataValidationException(noParent, noParent) } >> containerNodeFromYangUtils + when: 'attempt to parse some data' + def result = objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent) + then: 'the cache is cleared for the correct dataspace and schema' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + and: 'the result is the same container node as return from yang utils (no exception thrown!)' + assert result == containerNodeFromYangUtils + } + + def 'Parsing data with exception on all attempts.'() { + given: 'the yang parser always throws an exception' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> { throw new DataValidationException(noParent, noParent) } + when: 'attempt to parse some data' + objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the cache is cleared for the correct dataspace and schema (but that did not help)' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + } + +} diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy index e6344d3035..3852bae570 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2023 Nordix Foundation + * Copyright (C) 2020-2024 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG @@ -23,146 +23,10 @@ package org.onap.cps.utils -import org.onap.cps.TestUtils -import org.onap.cps.spi.exceptions.DataValidationException -import org.onap.cps.yang.YangTextSchemaSourceSetBuilder -import org.opendaylight.yangtools.yang.common.QName import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode import spock.lang.Specification class YangUtilsSpec extends Specification { - def 'Parsing a valid multicontainer Json String.'() { - given: 'a yang model (file)' - def jsonData = org.onap.cps.TestUtils.getResourceFileContent('multiple-object-data.json') - and: 'a model for that data' - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('multipleDataTree.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'the json data is parsed' - def result = YangUtils.parseJsonData(jsonData, schemaContext) - then: 'a ContainerNode holding collection of normalized nodes is returned' - result.body().getAt(index) instanceof NormalizedNode == true - then: 'qualified name of children created is as expected' - result.body().getAt(index).getIdentifier().nodeType == QName.create('org:onap:ccsdk:multiDataTree', '2020-09-15', nodeName) - where: - index | nodeName - 0 | 'first-container' - 1 | 'last-container' - } - - def 'Parsing a valid #scenario String.'() { - given: 'a yang model (file)' - def fileData = org.onap.cps.TestUtils.getResourceFileContent(contentFile) - and: 'a model for that data' - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'the data is parsed' - NormalizedNode result = YangUtils.parseData(contentType, fileData, schemaContext) - then: 'the result is a normalized node of the correct type' - if (revision) { - result.identifier.nodeType == QName.create(namespace, revision, localName) - } else { - result.identifier.nodeType == QName.create(namespace, localName) - } - where: - scenario | contentFile | contentType | namespace | revision | localName - 'JSON' | 'bookstore.json' | ContentType.JSON | 'org:onap:ccsdk:sample' | '2020-09-15' | 'bookstore' - 'XML' | 'bookstore.xml' | ContentType.XML | 'urn:ietf:params:xml:ns:netconf:base:1.0' | '' | 'bookstore' - } - - def 'Parsing invalid data: #description.'() { - given: 'a yang model (file)' - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'invalid data is parsed' - YangUtils.parseData(contentType, invalidData, schemaContext) - then: 'an exception is thrown' - thrown(DataValidationException) - where: 'the following invalid data is provided' - invalidData | contentType | description - '{incomplete json' | ContentType.JSON | 'incomplete json' - '{"test:bookstore": {"address": "Parnell st." }}' | ContentType.JSON | 'json with un-modelled data' - '{" }' | ContentType.JSON | 'json with syntax exception' - '' | ContentType.XML | 'incomplete xml' - 'blabla' | ContentType.XML | 'xml with invalid model' - '' | ContentType.XML | 'empty xml' - } - - def 'Parsing data fragment by xpath for #scenario.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - when: 'json string is parsed' - def result = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath) - then: 'a ContainerNode holding collection of normalized nodes is returned' - result.body().getAt(0) instanceof NormalizedNode == true - then: 'result represents a node of expected type' - result.body().getAt(0).getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName) - where: - scenario | contentType | nodeData | parentNodeXpath || nodeName - 'JSON list element as container' | ContentType.JSON | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch' - 'JSON list element within list' | ContentType.JSON | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch' - 'JSON container element' | ContentType.JSON | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest' - 'XML element test tree' | ContentType.XML | 'LeftSmallSparrow' | '/test-tree' || 'branch' - 'XML element branch xpath' | ContentType.XML | 'LeftSmallSparrowRobin' | '/test-tree' || 'branch' - 'XML container element' | ContentType.XML | 'SmallSparrow' | '/test-tree/branch[@name=\'Branch\']' || 'nest' - } - - def 'Parsing json data fragment by xpath error scenario: #scenario.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - when: 'json string is parsed' - YangUtils.parseJsonData('{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, - parentNodeXpath) - then: 'expected exception is thrown' - thrown(DataValidationException) - where: - scenario | parentNodeXpath - 'xpath has no identifiers' | '/' - 'xpath has no valid identifiers' | '/[@name=\'Name\']' - 'invalid parent path' | '/test-bush' - 'another invalid parent path' | '/test-tree/branch[@name=\'Branch\']/nest/name/last-name' - 'fragment does not belong to parent' | '/test-tree/' - } - - def 'Parsing json data with invalid json string: #description.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - when: 'malformed json string is parsed' - YangUtils.parseJsonData(invalidJson, schemaContext) - then: 'an exception is thrown' - thrown(DataValidationException) - where: 'the following malformed json is provided' - description | invalidJson - 'malformed json string with unterminated array data' | '{bookstore={categories=[{books=[{authors=[Iain M. Banks]}]}]}}' - 'incorrect json' | '{" }' - } - - def 'Parsing json data with space.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - and: 'some json data with space in the array elements' - def jsonDataWithSpacesInArrayElement = TestUtils.getResourceFileContent('bookstore.json') - when: 'that json data is parsed' - YangUtils.parseJsonData(jsonDataWithSpacesInArrayElement, schemaContext) - then: 'no exception thrown' - noExceptionThrown() - } - - def 'Parsing xPath to nodeId for #scenario.'() { - when: 'xPath is parsed' - def result = YangUtils.xpathToNodeIdSequence(xPath) - then: 'result represents an array of expected identifiers' - assert result == expectedNodeIdentifier - where: 'the following parameters are used' - scenario | xPath || expectedNodeIdentifier - 'container xpath' | '/test-tree' || ['test-tree'] - 'xpath contains list attribute' | '/test-tree/branch[@name=\'Branch\']' || ['test-tree','branch'] - 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] - } def 'Get key attribute statement without key attributes'() { given: 'a path argument without key attributes' -- cgit 1.2.3-korg