diff options
author | Arpit Singh <as00745003@techmahindra.com> | 2024-08-09 12:23:49 +0530 |
---|---|---|
committer | Arpit Singh <as00745003@techmahindra.com> | 2024-10-09 09:28:30 +0530 |
commit | 07acbb4ddd713f74406b156cbcac2507f96f3b08 (patch) | |
tree | 8f4122b9c3ecb994b9bb203cdd8b3fd1ad191f64 /cps-service | |
parent | e2517a8b993ed884edb251b91ce600d0a1a9fefe (diff) |
Implementation of Data validation feature in Create a Node API
Added support to validate JSON/XML data without the need of persisting
it in the databse.
- added "dryRunInQuery" flag as a new query parameter
- added new method as part of CpsDataService layer to perform data
validation
- added new method in yang parser "validateData" to validate
data without persisting it
Issue-ID: CPS-2361
Change-Id: I43dd33cc6120576d0fac606d5c4b0168d107311d
Signed-off-by: Arpit Singh <as00745003@techmahindra.com>
Diffstat (limited to 'cps-service')
11 files changed, 236 insertions, 32 deletions
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index 68e1880d77..b3eff8eb26 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -322,4 +322,18 @@ public interface CpsDataService { Map<String, String> yangResourcesNameToContentMap, String targetData, FetchDescendantsOption fetchDescendantsOption); + + + /** + * Validates JSON or XML data by parsing it using the schema associated to an anchor within the given dataspace. + * Validation is performed without persisting the data. + * + * @param dataspaceName the name of the dataspace where the anchor is located. + * @param anchorName the name of the anchor used to validate the data. + * @param parentNodeXpath the xpath of the parent node where the data is to be validated. + * @param nodeData the JSON or XML data to be validated. + * @param contentType the content type of the data (e.g., JSON or XML). + */ + void validateData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData, + ContentType contentType); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index eed4f09bf0..b1b545be68 100644 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -64,6 +64,7 @@ import org.springframework.stereotype.Service; public class CpsDataServiceImpl implements CpsDataService { private static final String ROOT_NODE_XPATH = "/"; + private static final String PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH = ""; private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L; private static final String NO_DATA_NODES = "No data nodes."; @@ -358,6 +359,14 @@ public class CpsDataServiceImpl implements CpsDataService { sendDataUpdatedEvent(anchor, listNodeXpath, Operation.DELETE, observedTimestamp); } + @Override + public void validateData(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final String nodeData, final ContentType contentType) { + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH : + CpsPathUtil.getNormalizedXpath(parentNodeXpath); + yangParser.validateData(contentType, nodeData, anchor, xpath); + } private Collection<DataNode> rebuildSourceDataNodes(final String xpath, final Anchor sourceAnchor, final Collection<DataNode> sourceDataNodes) { @@ -422,7 +431,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final ContentType contentType) { if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, ""); + final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, + anchor, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); final Collection<DataNode> dataNodes = new DataNodeBuilder() .withContainerNode(containerNode) .buildCollection(); @@ -450,7 +460,7 @@ public class CpsDataServiceImpl implements CpsDataService { if (isRootNodeXpath(xpath)) { final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - yangResourcesNameToContentMap, ""); + yangResourcesNameToContentMap, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); final Collection<DataNode> dataNodes = new DataNodeBuilder() .withContainerNode(containerNode) .buildCollection(); diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java index dc23c6bc4a..168e0999d5 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java @@ -21,6 +21,9 @@ package org.onap.cps.utils; +import static org.onap.cps.utils.YangParserHelper.VALIDATE_AND_PARSE; +import static org.onap.cps.utils.YangParserHelper.VALIDATE_ONLY; + import io.micrometer.core.annotation.Timed; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -57,11 +60,12 @@ public class YangParser { final String parentNodeXpath) { final SchemaContext schemaContext = getSchemaContext(anchor); try { - return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return yangParserHelper + .parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE); } catch (final DataValidationException e) { invalidateCache(anchor); } - return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE); } /** @@ -78,7 +82,31 @@ public class YangParser { final Map<String, String> yangResourcesNameToContentMap, final String parentNodeXpath) { final SchemaContext schemaContext = getSchemaContext(yangResourcesNameToContentMap); - return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE); + } + + /** + * Parses data to validate it, using the schema context for given anchor. + * + * @param anchor the anchor used for node data validation + * @param parentNodeXpath the xpath of the parent node + * @param nodeData JSON or XML data string to validate + * @param contentType the content type of the data (e.g., JSON or XML) + * @throws DataValidationException if validation fails + */ + public void validateData(final ContentType contentType, + final String nodeData, + final Anchor anchor, + final String parentNodeXpath) { + final SchemaContext schemaContext = getSchemaContext(anchor); + try { + yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_ONLY); + } catch (final DataValidationException e) { + invalidateCache(anchor); + log.error("Data validation failed for anchor: {}, xpath: {}, details: {}", anchor, parentNodeXpath, + e.getMessage()); + } + yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_ONLY); } private SchemaContext getSchemaContext(final Anchor anchor) { diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java b/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java index d95aceaf79..5612945ea9 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java @@ -72,6 +72,9 @@ public class YangParserHelper { static final String DATA_ROOT_NODE_NAMESPACE = "urn:ietf:params:xml:ns:netconf:base:1.0"; static final String DATA_ROOT_NODE_TAG_NAME = "data"; + static final String DATA_VALIDATION_FAILURE_MESSAGE = "Data Validation Failed"; + static final boolean VALIDATE_ONLY = true; + static final boolean VALIDATE_AND_PARSE = false; /** * Parses data into NormalizedNode according to given schema context. @@ -85,11 +88,20 @@ public class YangParserHelper { public ContainerNode parseData(final ContentType contentType, final String nodeData, final SchemaContext schemaContext, - final String parentNodeXpath) { + final String parentNodeXpath, + final boolean validateOnly) { if (contentType == ContentType.JSON) { - return parseJsonData(nodeData, schemaContext, parentNodeXpath); + final ContainerNode validatedAndParsedJson = parseJsonData(nodeData, schemaContext, parentNodeXpath); + if (validateOnly) { + return null; + } + return validatedAndParsedJson; + } + final NormalizedNodeResult normalizedNodeResult = parseXmlData(nodeData, schemaContext, parentNodeXpath); + if (validateOnly) { + return null; } - return parseXmlData(nodeData, schemaContext, parentNodeXpath); + return buildContainerNodeFormNormalizedNodeResult(normalizedNodeResult); } private ContainerNode parseJsonData(final String jsonData, @@ -124,13 +136,13 @@ public class YangParserHelper { jsonParserStream.parse(jsonReader); } catch (final IOException | JsonSyntaxException | IllegalStateException | IllegalArgumentException exception) { throw new DataValidationException( - "Data Validation Failed", "Failed to parse json data. " + exception.getMessage(), exception); + DATA_VALIDATION_FAILURE_MESSAGE, "Failed to parse json data. " + exception.getMessage(), exception); } return dataContainerNodeBuilder.build(); } @SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION", justification = "Problem originates in 3PP code") - private ContainerNode parseXmlData(final String xmlData, + private NormalizedNodeResult parseXmlData(final String xmlData, final SchemaContext schemaContext, final String parentNodeXpath) { final XMLInputFactory factory = XMLInputFactory.newInstance(); @@ -167,12 +179,17 @@ public class YangParserHelper { } catch (final XMLStreamException | URISyntaxException | IOException | SAXException | NullPointerException | ParserConfigurationException | TransformerException exception) { throw new DataValidationException( - "Data Validation Failed", "Failed to parse xml data: " + exception.getMessage(), exception); + DATA_VALIDATION_FAILURE_MESSAGE, "Failed to parse xml data: " + exception.getMessage(), exception); } + return normalizedNodeResult; + } + + private ContainerNode buildContainerNodeFormNormalizedNodeResult(final NormalizedNodeResult normalizedNodeResult) { + final DataContainerChild dataContainerChild = - (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult()); + (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult()); final YangInstanceIdentifier.NodeIdentifier nodeIdentifier = - new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType()); + new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType()); return Builders.containerBuilder().withChild(dataContainerChild).withNodeIdentifier(nodeIdentifier).build(); } 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 9846b30158..8c208a1cf8 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 @@ -546,6 +546,20 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockDataUpdateEventsService.publishCpsDataUpdateEvent(anchor2, '/', DELETE, observedTimestamp) } + def "Validating #scenario when dry run is enabled."() { + given: 'schema set for given anchors and dataspace references bookstore model' + setupSchemaSetMocks('bookstore.yang') + when: 'validating the data with the given parameters' + objectUnderTest.validateData(dataspaceName, anchorName, parentNodeXpath, data,contentType) + then: 'the appropriate yang parser method is invoked with correct parameters' + yangParser.validateData(contentType, data, anchor, xpath) + where: 'the following parameters were used' + scenario | parentNodeXpath | xpath | contentType | data + 'JSON data with root node xpath' | '/' | '' | ContentType.JSON | '{"bookstore":{"bookstore-name":"Easons"}}' + 'JSON data with specific xpath' | '/bookstore' | '/bookstore' | ContentType.JSON | '{"bookstore-name":"Easons"}' + 'XML data with specific xpath' | '/bookstore' | '/bookstore' | ContentType.XML | '<bookstore-name>Easons</bookstore-name>' + } + def 'Start session.'() { when: 'start session method is called' objectUnderTest.startSession() 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 05c8983fc2..9f3456280e 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 @@ -171,6 +171,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'
- new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '') != null
+ new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '', false) != 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 e305abee86..f028d5d5d9 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 @@ -35,6 +35,7 @@ class DataNodeBuilderSpec extends Specification { def objectUnderTest = new DataNodeBuilder() def yangParserHelper = new YangParserHelper() + def validateAndParse = false def expectedLeavesByXpathMap = [ '/test-tree' : [], @@ -60,7 +61,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 = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -80,7 +81,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 = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree', validateAndParse) 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) @@ -96,7 +97,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 = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) when: 'the container node is converted to a data node ' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -129,7 +130,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 = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath, validateAndParse) 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' @@ -144,7 +145,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 = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -162,7 +163,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 = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath, validateAndParse) 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/YangParserHelperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy index 073383113d..e1490c28ab 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy @@ -30,6 +30,8 @@ import spock.lang.Specification class YangParserHelperSpec extends Specification { def objectUnderTest = new YangParserHelper() + def validateOnly = true + def validateAndParse = false def 'Parsing a valid multicontainer Json String.'() { given: 'a yang model (file)' @@ -38,7 +40,7 @@ class YangParserHelperSpec extends Specification { 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, '') + def result = objectUnderTest.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) 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' @@ -56,7 +58,7 @@ class YangParserHelperSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() when: 'the data is parsed' - NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '') + NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '', validateAndParse) then: 'the result is a normalized node of the correct type' if (revision) { result.identifier.nodeType == QName.create(namespace, revision, localName) @@ -74,7 +76,7 @@ class YangParserHelperSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() when: 'invalid data is parsed' - objectUnderTest.parseData(contentType, invalidData, schemaContext, '') + objectUnderTest.parseData(contentType, invalidData, schemaContext, '', validateAndParse) then: 'an exception is thrown' thrown(DataValidationException) where: 'the following invalid data is provided' @@ -92,7 +94,7 @@ class YangParserHelperSpec extends Specification { 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) + def result = objectUnderTest.parseData(contentType, nodeData, schemaContext, parentNodeXpath, validateAndParse) 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' @@ -112,7 +114,7 @@ class YangParserHelperSpec extends Specification { 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) + objectUnderTest.parseData(ContentType.JSON, '{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, parentNodeXpath, validateAndParse) then: 'expected exception is thrown' thrown(DataValidationException) where: @@ -129,7 +131,7 @@ class YangParserHelperSpec extends Specification { def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() when: 'malformed json string is parsed' - objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '') + objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '', validateAndParse) then: 'an exception is thrown' thrown(DataValidationException) where: 'the following malformed json is provided' @@ -145,7 +147,7 @@ class YangParserHelperSpec extends Specification { 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, '') + objectUnderTest.parseData(ContentType.JSON, jsonDataWithSpacesInArrayElement, schemaContext, '', validateAndParse) then: 'no exception thrown' noExceptionThrown() } @@ -162,5 +164,22 @@ class YangParserHelperSpec extends Specification { 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] } + def 'Validating #scenario xpath String.'() { + given: 'a data model (file) is provided' + def fileData = TestUtils.getResourceFileContent(contentFile) + and: 'the schema context is built for that data model' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'the data is parsed to be validated' + objectUnderTest.parseData(contentType, fileData, schemaContext, parentNodeXpath, validateOnly) + then: 'no exception is thrown' + noExceptionThrown() + where: + scenario | parentNodeXpath | contentFile | contentType + 'JSON without parent node' | '' | 'bookstore.json' | ContentType.JSON + 'JSON with parent node' | '/bookstore' | 'bookstore-categories-data.json' | ContentType.JSON + 'XML without parent node' | '' | 'bookstore.xml' | ContentType.XML + 'XML with parent node' | '/bookstore' | 'bookstore-categories-data.xml' | ContentType.XML + } } 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 index 18d0502e30..6c52becbe1 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy @@ -26,7 +26,6 @@ import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.Anchor import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSet -import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode import org.opendaylight.yangtools.yang.model.api.SchemaContext import spock.lang.Specification @@ -47,6 +46,8 @@ class YangParserSpec extends Specification { def containerNodeFromYangUtils = Mock(ContainerNode) def noParent = '' + def validateOnly = true + def validateAndParse = false def setup() { mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet @@ -55,7 +56,7 @@ class YangParserSpec extends Specification { def 'Parsing data.'() { given: 'the yang parser (utility) always returns a container node' - mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> 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' @@ -68,7 +69,7 @@ class YangParserSpec extends Specification { 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 + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> { 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' @@ -79,7 +80,7 @@ class YangParserSpec extends Specification { 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) } + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> { 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' @@ -94,9 +95,46 @@ class YangParserSpec extends Specification { when: 'parsing some json data' def result = objectUnderTest.parseData(ContentType.JSON, 'some json', yangResourcesNameToContentMap, noParent) then: 'the yang parser helper always returns a container node' - 1 * mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils + 1 * mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> containerNodeFromYangUtils and: 'the result is the same container node as return from yang utils' assert result == containerNodeFromYangUtils } + def 'Validating #scenario data using Yang parser with cache retrieval.'() { + given: 'the yang parser (utility) is set up and schema context is available' + mockYangParserHelper.parseData(contentType, 'some json', mockSchemaContext, noParent, validateOnly) + when: 'attempt to parse data with no parent node xpath' + objectUnderTest.validateData(contentType, 'some json or xml data', anchor, noParent) + then: 'the correct schema set is retrieved from the cache for the dataspace and schema' + 1 * mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet + and: 'no cache entries are removed during validation' + 0 * mockYangTextSchemaSourceSetCache.removeFromCache(*_) + where: + scenario | contentType + 'JSON' | ContentType.JSON + 'XML' | ContentType.XML + } + + def 'Validating data when parsing fails on first attempt and recovers.'() { + given: 'the Yang parser throws an exception on the first attempt but succeeds on the second' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateOnly) >> { throw new DataValidationException(noParent, noParent) } >> null + when: 'attempting to parse JSON data' + objectUnderTest.validateData(ContentType.JSON, 'some json', anchor, noParent) + then: 'the cache is cleared for the correct dataspace and schema after the first failure' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + and: 'no exceptions are thrown after the second attempt' + noExceptionThrown() + } + + def 'Validating data with repeated parsing failures leading to exception.'() { + given: 'the yang parser throws an exception on the first attempt only' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateOnly) >> { throw new DataValidationException(noParent, noParent) } + when: 'attempting to parse JSON data' + objectUnderTest.validateData(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 after the failure' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + } + } diff --git a/cps-service/src/test/resources/bookstore-categories-data.json b/cps-service/src/test/resources/bookstore-categories-data.json new file mode 100644 index 0000000000..7dc22b17f7 --- /dev/null +++ b/cps-service/src/test/resources/bookstore-categories-data.json @@ -0,0 +1,49 @@ +{ + "categories": [ + { + "code": "01/1", + "name": "SciFi", + "books": [ + { + "authors": [ + "Iain M. Banks" + ], + "lang": "en/it", + "price": "895", + "pub_year": "1994", + "title": "Feersum Endjinn/Endjinn Feersum" + }, + { + "authors": [ + "Ursula K. Le Guin", + "Joe Haldeman", + "Orson Scott Card", + "david Brin", + "Rober Silverberg", + "Dan Simmons", + "Greg Bear" + ], + "lang": "en", + "price": "1099", + "pub_year": "1999", + "title": "Far Horizons" + } + ] + }, + { + "name": "kids", + "code": "02", + "books": [ + { + "authors": [ + "Philip Pullman" + ], + "lang": "en", + "price": "699", + "pub_year": "1995", + "title": "The Golden Compass" + } + ] + } + ] +}
\ No newline at end of file diff --git a/cps-service/src/test/resources/bookstore-categories-data.xml b/cps-service/src/test/resources/bookstore-categories-data.xml new file mode 100644 index 0000000000..c8592c1f90 --- /dev/null +++ b/cps-service/src/test/resources/bookstore-categories-data.xml @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='UTF-8'?> +<categories> + <code>1</code> + <name>SciFi</name> + <books> + <title>2001: A Space Odyssey</title> + <lang>en</lang> + <authors> + Iain M. Banks + </authors> + <pub_year>1994</pub_year> + <price>895</price> + </books> +</categories>
\ No newline at end of file |