diff options
Diffstat (limited to 'cps-service')
21 files changed, 426 insertions, 188 deletions
diff --git a/cps-service/pom.xml b/cps-service/pom.xml index c97623f2a1..8bc39b1d48 100644 --- a/cps-service/pom.xml +++ b/cps-service/pom.xml @@ -35,10 +35,6 @@ <artifactId>cps-service</artifactId> - <properties> - <minimum-coverage>0.95</minimum-coverage> - </properties> - <dependencies> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> diff --git a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java index 38f8988279..696fd60f8c 100644 --- a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java +++ b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (c) 2021-2022 Bell Canada. - * Modifications Copyright (c) 2022 Nordix Foundation + * Modifications Copyright (c) 2022-2023 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; import lombok.AllArgsConstructor; +import lombok.SneakyThrows; import org.onap.cps.api.CpsDataService; import org.onap.cps.event.model.Content; import org.onap.cps.event.model.CpsDataUpdatedEvent; @@ -44,22 +45,9 @@ import org.springframework.stereotype.Component; @AllArgsConstructor(onConstructor = @__(@Lazy)) public class CpsDataUpdatedEventFactory { - private static final URI EVENT_SCHEMA; - private static final URI EVENT_SOURCE; - private static final String EVENT_TYPE = "org.onap.cps.data-updated-event"; private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - static { - try { - EVENT_SCHEMA = new URI("urn:cps:org.onap.cps:data-updated-event-schema:v1"); - EVENT_SOURCE = new URI("urn:cps:org.onap.cps"); - } catch (final URISyntaxException e) { - // As it is fixed string, I don't expect to see this error - throw new IllegalArgumentException(e); - } - } - @Lazy private final CpsDataService cpsDataService; @@ -82,14 +70,17 @@ public class CpsDataUpdatedEventFactory { return toCpsDataUpdatedEvent(anchor, dataNode, observedTimestamp, operation); } - private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final DataNode dataNode, - final OffsetDateTime observedTimestamp, final Operation operation) { - final var cpsDataUpdatedEvent = new CpsDataUpdatedEvent(); + @SneakyThrows(URISyntaxException.class) + private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, + final DataNode dataNode, + final OffsetDateTime observedTimestamp, + final Operation operation) { + final CpsDataUpdatedEvent cpsDataUpdatedEvent = new CpsDataUpdatedEvent(); cpsDataUpdatedEvent.withContent(createContent(anchor, dataNode, observedTimestamp, operation)); cpsDataUpdatedEvent.withId(UUID.randomUUID().toString()); - cpsDataUpdatedEvent.withSchema(EVENT_SCHEMA); - cpsDataUpdatedEvent.withSource(EVENT_SOURCE); - cpsDataUpdatedEvent.withType(EVENT_TYPE); + cpsDataUpdatedEvent.withSchema(new URI("urn:cps:org.onap.cps:data-updated-event-schema:v1")); + cpsDataUpdatedEvent.withSource(new URI("urn:cps:org.onap.cps")); + cpsDataUpdatedEvent.withType("org.onap.cps.data-updated-event"); return cpsDataUpdatedEvent; } diff --git a/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java b/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java index 02574995dc..3b90b06cb0 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java +++ b/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java @@ -42,7 +42,7 @@ public class FetchDescendantsOption { } private static final Pattern FETCH_DESCENDANTS_OPTION_PATTERN = - Pattern.compile("^$|^all$|^none$|^[0-9]+$|^-1$"); + Pattern.compile("^$|^all$|^none$|^direct$|^[0-9]+$|^-1$|^1$"); private final int depth; @@ -96,6 +96,8 @@ public class FetchDescendantsOption { return FetchDescendantsOption.OMIT_DESCENDANTS; } else if ("-1".equals(fetchDescendantsOptionAsString) || "all".equals(fetchDescendantsOptionAsString)) { return FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; + } else if ("1".equals(fetchDescendantsOptionAsString) || "direct".equals(fetchDescendantsOptionAsString)) { + return FetchDescendantsOption.DIRECT_CHILDREN_ONLY; } else { final Integer depth = Integer.valueOf(fetchDescendantsOptionAsString); return new FetchDescendantsOption(depth); diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java index e212933388..b040af5bb4 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java @@ -184,9 +184,8 @@ public class DataNodeBuilder { private DataNode buildFromContainerNode() { final Collection<DataNode> dataNodeCollection = buildCollectionFromContainerNode(); - if (!dataNodeCollection.iterator().hasNext()) { - throw new DataValidationException( - "Unsupported xpath: ", "Unsupported xpath as it is referring to one element"); + if (dataNodeCollection.isEmpty()) { + throw new DataValidationException("Unsupported Normalized Node", "No valid node found"); } return dataNodeCollection.iterator().next(); } @@ -278,5 +277,4 @@ public class DataNodeBuilder { } } - } diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java index 09f2e16c6a..98c7947e1c 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java +++ b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG + * Modifications Copyright (C) 2023 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +40,6 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import org.onap.cps.spi.exceptions.DataValidationException; import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.w3c.dom.Document; @@ -102,10 +102,8 @@ public class XmlFileUtils { final Map<String, String> rootNodeProperty) throws IOException, SAXException, ParserConfigurationException, TransformerException { final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); - final StringBuilder xmlStringBuilder = new StringBuilder(); - xmlStringBuilder.append(xmlContent); - final Document document = documentBuilder.parse( - new ByteArrayInputStream(xmlStringBuilder.toString().getBytes(StandardCharsets.UTF_8))); + final Document document = + documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8))); final Element root = document.getDocumentElement(); if (!root.getTagName().equals(rootNodeTagName) && !root.getTagName().equals(YangUtils.DATA_ROOT_NODE_TAG_NAME)) { @@ -143,22 +141,19 @@ public class XmlFileUtils { static Document addDataRootNode(final Element node, final String tagName, final String namespace, - final Map<String, String> rootNodeProperty) { - try { - final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); - final Document document = documentBuilder.newDocument(); - final Element rootElement = document.createElementNS(namespace, tagName); - for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) { - final Element propertyElement = document.createElement(entry.getKey()); - propertyElement.setTextContent(entry.getValue()); - rootElement.appendChild(propertyElement); - } - rootElement.appendChild(document.adoptNode(node)); - document.appendChild(rootElement); - return document; - } catch (final ParserConfigurationException exception) { - throw new DataValidationException("Can't parse XML", "XML can't be parsed", exception); + final Map<String, String> rootNodeProperty) + throws ParserConfigurationException { + final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); + final Document document = documentBuilder.newDocument(); + final Element rootElement = document.createElementNS(namespace, tagName); + for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) { + final Element propertyElement = document.createElement(entry.getKey()); + propertyElement.setTextContent(entry.getValue()); + rootElement.appendChild(propertyElement); } + rootElement.appendChild(document.adoptNode(node)); + document.appendChild(rootElement); + return document; } private static DocumentBuilderFactory getDocumentBuilderFactory() { diff --git a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java index deb5b05752..ca907148dd 100644 --- a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java +++ b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java @@ -27,7 +27,6 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import io.micrometer.core.annotation.Timed; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -37,7 +36,6 @@ import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.NoArgsConstructor; -import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.spi.exceptions.ModelValidationException; import org.onap.cps.spi.model.ModuleReference; import org.opendaylight.yangtools.yang.common.Revision; @@ -45,7 +43,6 @@ import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier; import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource; -import org.opendaylight.yangtools.yang.parser.api.YangSyntaxErrorException; import org.opendaylight.yangtools.yang.parser.rfc7950.reactor.RFC7950Reactors; import org.opendaylight.yangtools.yang.parser.rfc7950.repo.YangStatementStreamSource; import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; @@ -144,23 +141,20 @@ public final class YangTextSchemaSourceSetBuilder { final String resourceName = yangTextSchemaSource.getIdentifier().getName(); try { reactor.addSource(YangStatementStreamSource.create(yangTextSchemaSource)); - } catch (final IOException e) { - throw new CpsException("Failed to read yang resource.", - String.format("Exception occurred on reading resource %s.", resourceName), e); - } catch (final YangSyntaxErrorException e) { - throw new ModelValidationException("Yang resource is invalid.", - String.format( - "Yang syntax validation failed for resource %s:%n%s", resourceName, e.getMessage()), e); + } catch (final Exception exception) { + throw new ModelValidationException("Yang resource processing exception.", + String.format("Could not process resource %s:%n%s", resourceName, exception.getMessage()), + exception); } } try { return reactor.buildEffective(); - } catch (final ReactorException e) { + } catch (final ReactorException reactorException) { final List<String> resourceNames = yangResourceNameToContent.keySet().stream().collect(Collectors.toList()); Collections.sort(resourceNames); throw new ModelValidationException("Invalid schema set.", - String.format("Effective schema context build failed for resources %s.", resourceNames.toString()), - e); + String.format("Effective schema context build failed for resources %s.", resourceNames), + reactorException); } } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy index 4e0349d2b8..eb41e2085f 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy @@ -25,6 +25,7 @@ package org.onap.cps.api.impl import org.onap.cps.api.CpsDataService import org.onap.cps.spi.CpsAdminPersistenceService +import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.Dataspace import org.onap.cps.spi.utils.CpsValidator @@ -154,6 +155,21 @@ class CpsAdminServiceImplSpec extends Specification { 1 * mockCpsValidator.validateNameCharacters('some-dataspace-name') } + def 'Query all anchors with Module Names Not Found Exception in persistence layer.'() { + given: 'the persistence layer throws a Module Names Not Found Exception' + def originalException = new ModuleNamesNotFoundException('exception-ds', [ 'm1', 'm2']) + mockCpsAdminPersistenceService.queryAnchors(*_) >> { throw originalException} + when: 'attempt query anchors' + objectUnderTest.queryAnchorNames('some-dataspace-name', []) + then: 'the same exception is thrown (up)' + def thrownUp = thrown(ModuleNamesNotFoundException) + assert thrownUp == originalException + and: 'the exception details contains the relevant data' + assert thrownUp.details.contains('exception-ds') + assert thrownUp.details.contains('m1') + assert thrownUp.details.contains('m2') + } + def 'Delete dataspace.'() { when: 'delete dataspace is invoked' objectUnderTest.deleteDataspace('someDataspace') 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 9d241f1bde..b4ac7a68f3 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 @@ -29,7 +29,11 @@ import org.onap.cps.notification.NotificationService import org.onap.cps.notification.Operation import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.exceptions.ConcurrencyException +import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch 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 @@ -333,6 +337,18 @@ class CpsDataServiceImplSpec extends Specification { 'level 2 node' | ['/test-tree' : '{"branch": [{"name":"Name"}]}', '/test-tree/branch[@name=\'Name\']':'{"nest":{"name":"nestName"}}'] || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"] } + def 'Replace data node with concurrency exception in persistence layer.'() { + given: 'the persistence layer throws an concurrency exception' + def originalException = new ConcurrencyException('message', 'details') + mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException } + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to replace data node' + objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp) + then: 'the same exception is thrown up' + def thrownUp = thrown(ConcurrencyException) + assert thrownUp == originalException + } + def 'Replace list content data fragment under parent node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -366,8 +382,6 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete list element under existing node.'() { - given: 'schema set for given anchor and dataspace references test-tree model' - setupSchemaSetMocks('test-tree.yang') when: 'delete list data method is invoked with list element json data' objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp) then: 'the persistence service method is invoked with correct parameters' @@ -379,8 +393,6 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete multiple list elements under existing node.'() { - given: 'schema set for given anchor and dataspace references test-tree model' - setupSchemaSetMocks('test-tree.yang') when: 'delete multiple list data method is invoked with list element json data' objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp) then: 'the persistence service method is invoked with correct parameters' @@ -392,8 +404,6 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete data node under anchor and dataspace.'() { - given: 'schema set for given anchor and dataspace references test tree model' - setupSchemaSetMocks('test-tree.yang') when: 'delete data node method is invoked with correct parameters' objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp) then: 'the persistence service method is invoked with the correct parameters' @@ -405,9 +415,7 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete all data nodes for a given anchor and dataspace.'() { - given: 'schema set for given anchor and dataspace references test tree model' - setupSchemaSetMocks('test-tree.yang') - when: 'delete data node method is invoked with correct parameters' + when: 'delete data nodes method is invoked with correct parameters' objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp) then: 'data updated event is sent to notification service before the delete' 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp) @@ -417,6 +425,20 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName) } + def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() { + given: 'a batch exception in persistence layer' + def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[]) + mockCpsDataPersistenceService.deleteDataNodes(*_) >> { throw originalException } + when: 'attempt to delete data nodes' + objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp) + then: 'the original exception is thrown up' + def thrownUp = thrown(DataNodeNotFoundExceptionBatch) + assert thrownUp == originalException + and: 'the exception details contain the expected data' + assert thrownUp.details.contains('ds1') + assert thrownUp.details.contains('a1') + } + def 'Delete all data nodes for given dataspace and multiple anchors.'() { given: 'schema set for given anchors and dataspace references test tree model' setupSchemaSetMocks('test-tree.yang') @@ -433,22 +455,28 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>) } - def setupSchemaSetMocks(String... yangResources) { - def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) - mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext - } - - def 'start session'() { + def 'Start session.'() { when: 'start session method is called' objectUnderTest.startSession() then: 'the persistence service method to start session is invoked' 1 * mockCpsDataPersistenceService.startSession() } - def 'close session'(){ + def 'Start session with Session Manager Exceptions.'() { + given: 'the persistence layer throws an Session Manager Exception' + mockCpsDataPersistenceService.startSession() >> { throw originalException } + when: 'attempt to start session' + objectUnderTest.startSession() + then: 'the original exception is thrown up' + def thrownUp = thrown(SessionManagerException) + assert thrownUp == originalException + where: 'variations of Session Manager Exception are used' + originalException << [ new SessionManagerException('message','details'), + new SessionManagerException('message','details', new Exception('cause')), + new SessionTimeoutException('message','details', new Exception('cause'))] + } + + def 'Close session.'(){ given: 'session Id from calling the start session method' def sessionId = objectUnderTest.startSession() when: 'close session method is called' @@ -457,20 +485,26 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.closeSession(sessionId) } - def 'lock anchor with no timeout parameter'(){ + def 'Lock anchor with no timeout parameter.'(){ when: 'lock anchor method with no timeout parameter with details of anchor entity to lock' objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName') then: 'the persistence service method to lock anchor is invoked with default timeout' - 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', - 'some-anchorName', 300L) + 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L) } - def 'lock anchor with timeout parameter'(){ + def 'Lock anchor with timeout parameter.'(){ when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock' - objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', - 'some-anchorName', 250L) + objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L) then: 'the persistence service method to lock anchor is invoked with the given timeout' - 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', - 'some-anchorName', 250L) + 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L) + } + + def setupSchemaSetMocks(String... yangResources) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext } + } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy index 3884eda661..a794c58fc6 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy @@ -26,8 +26,10 @@ package org.onap.cps.api.impl import org.onap.cps.TestUtils import org.onap.cps.api.CpsAdminService import org.onap.cps.spi.CpsModulePersistenceService +import org.onap.cps.spi.exceptions.DuplicatedYangResourceException import org.onap.cps.spi.exceptions.ModelValidationException import org.onap.cps.spi.exceptions.SchemaSetInUseException +import org.onap.cps.spi.model.ModuleDefinition import org.onap.cps.spi.utils.CpsValidator import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.ModuleReference @@ -50,24 +52,22 @@ class CpsModuleServiceImplSpec extends Specification { def objectUnderTest = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAdminService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) def 'Create schema set.'() { - given: 'Valid yang resource as name-to-content map' - def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') when: 'Create schema set method is invoked' - objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap) + objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', [:]) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap) + 1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', [:]) and: 'the CpsValidator is called on the dataspaceName and schemaSetName' 1 * mockCpsValidator.validateNameCharacters('someDataspace', 'someSchemaSet') } def 'Create schema set from new modules and existing modules.'() { given: 'a list of existing modules module reference' - def moduleReferenceForExistingModule = new ModuleReference("test", "2021-10-12","test.org") + def moduleReferenceForExistingModule = new ModuleReference('test', '2021-10-12','test.org') def listOfExistingModulesModuleReference = [moduleReferenceForExistingModule] when: 'create schema set from modules method is invoked' - objectUnderTest.createSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference) + objectUnderTest.createSchemaSetFromModules('someDataspaceName', 'someSchemaSetName', [newModule: 'newContent'], listOfExistingModulesModuleReference) then: 'processing is delegated to persistence service' - 1 * mockCpsModulePersistenceService.storeSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference) + 1 * mockCpsModulePersistenceService.storeSchemaSetFromModules('someDataspaceName', 'someSchemaSetName', [newModule: 'newContent'], listOfExistingModulesModuleReference) and: 'the CpsValidator is called on the dataspaceName and schemaSetName' 1 * mockCpsValidator.validateNameCharacters('someDataspaceName', 'someSchemaSetName') } @@ -78,7 +78,21 @@ class CpsModuleServiceImplSpec extends Specification { when: 'Create schema set method is invoked' objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap) then: 'Model validation exception is thrown' - thrown(ModelValidationException.class) + thrown(ModelValidationException) + } + + def 'Create schema set with duplicate yang resource exception in persistence layer.'() { + given: 'the persistence layer throws an duplicated yang resource exception' + def originalException = new DuplicatedYangResourceException('name', '123', null) + mockCpsModulePersistenceService.storeSchemaSet(*_) >> { throw originalException } + when: 'attempt to create schema set' + objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', [:]) + then: 'the same duplicated yang resource exception is thrown (up)' + def thrownUp = thrown(DuplicatedYangResourceException) + assert thrownUp == originalException + and: 'the exception message contains the relevant data' + assert thrownUp.message.contains('name') + assert thrownUp.message.contains('123') } def 'Get schema set by name and dataspace.'() { @@ -212,20 +226,23 @@ class CpsModuleServiceImplSpec extends Specification { 1 * mockCpsValidator.validateNameCharacters('someDataspaceName', 'someAnchorName') } - def 'Identifying new module references'(){ + def 'Identifying new module references.'(){ given: 'module references from cm handle' def moduleReferencesToCheck = [new ModuleReference('some-module', 'some-revision')] when: 'identifyNewModuleReferences is called' objectUnderTest.identifyNewModuleReferences(moduleReferencesToCheck) then: 'cps module persistence service is called with module references to check' - 1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck); + 1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck) } def 'Getting module definitions.'() { + given: 'the module persistence service returns a collection of module definitions' + def moduleDefinitionsFromPersistenceService = [ new ModuleDefinition('name', 'revision', 'content' ) ] + mockCpsModulePersistenceService.getYangResourceDefinitions('some-dataspace-name', 'some-anchor-name') >> moduleDefinitionsFromPersistenceService when: 'get module definitions method is called with a valid dataspace and anchor name' - objectUnderTest.getModuleDefinitionsByAnchorName('some-dataspace-name', 'some-anchor-name') - then: 'CPS module persistence service is invoked the correct number of times' - 1 * mockCpsModulePersistenceService.getYangResourceDefinitions('some-dataspace-name', 'some-anchor-name') + def result = objectUnderTest.getModuleDefinitionsByAnchorName('some-dataspace-name', 'some-anchor-name') + then: 'the result is the same collection returned by the persistence service' + assert result == moduleDefinitionsFromPersistenceService and: 'the CpsValidator is called on the dataspaceName and schemaSetName' 1 * mockCpsValidator.validateNameCharacters('some-dataspace-name', 'some-anchor-name') } diff --git a/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy new file mode 100644 index 0000000000..8efd48547e --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 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.cache + +import spock.lang.Specification + +class HazelcastCacheConfigSpec extends Specification { + + def objectUnderTest = new HazelcastCacheConfig() + + def 'Create Hazelcast instance with a #scenario'() { + given: 'a cluster name' + objectUnderTest.clusterName = 'my cluster' + when: 'an hazelcast instance is created (name has to be unique)' + def result = objectUnderTest.createHazelcastInstance(scenario, config) + then: 'the instance is created and has the correct name' + assert result.name == scenario + and: 'if applicable it has a map config with the expected name' + if (expectMapConfig) { + assert result.config.mapConfigs.values()[0].name == 'my map config' + } else { + assert result.config.mapConfigs.isEmpty() + } + and: 'if applicable it has a queue config with the expected name' + if (expectQueueConfig) { + assert result.config.queueConfigs.values()[0].name == 'my queue config' + } else { + assert result.config.queueConfigs.isEmpty() + } + where: 'the following configs are used' + scenario | config || expectMapConfig | expectQueueConfig + 'Map Config' | HazelcastCacheConfig.createMapConfig('my map config') || true | false + 'Queue Config' | HazelcastCacheConfig.createQueueConfig('my queue config') || false | true + } + +} diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java b/cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy index 6a4e2a098f..b1880d50fb 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java +++ b/cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy @@ -18,23 +18,15 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.spi.exceptions; +package org.onap.cps.config -/** - * Operation Not Yet Supported Exception. - * Indicates the operation is not supported and has intention to be supported in the future. - */ - -public class OperationNotYetSupportedException extends CpsException { +import spock.lang.Specification - private static final long serialVersionUID = 1517903069236383746L; +class CacheConfigSpec extends Specification { - /** - * Constructor. - * - * @param details reason for the exception - */ - public OperationNotYetSupportedException(final String details) { - super("Operation Not Yet Supported Exception", details); + def 'Create Cache Config. (easiest test ever)'() { + expect: 'can create a Cache Config' + new CacheConfig() != null } + } diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy index 5dbc2bb04b..49f4bf3850 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (c) 2021-2022 Bell Canada. - * Modifications Copyright (c) 2022 Nordix Foundation + * Modifications Copyright (c) 2022-2023 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,8 @@ package org.onap.cps.notification +import org.onap.cps.spi.model.DataNode + import java.time.OffsetDateTime import java.time.format.DateTimeFormatter import org.onap.cps.utils.DateTimeUtility @@ -35,7 +37,7 @@ import org.onap.cps.spi.model.DataNodeBuilder import org.springframework.util.StringUtils import spock.lang.Specification -class CpsDataUpdateEventFactorySpec extends Specification { +class CpsDataUpdatedEventFactorySpec extends Specification { def mockCpsDataService = Mock(CpsDataService) @@ -112,6 +114,22 @@ class CpsDataUpdateEventFactorySpec extends Specification { } } + def 'Create CPS Data Event with URI Syntax Exception'() { + given: 'an anchor' + def anchor = new Anchor('my-anchorname', 'my-dataspace', 'my-schemaset-name') + and: 'a mocked data Node (collection)' + def mockDataNode = Mock(DataNode) + mockCpsDataService.getDataNodes(*_) >> [ mockDataNode ] + and: 'a URI syntax exception is thrown somewhere (using datanode as cannot manipulate hardcoded URIs' + def originalException = new URISyntaxException('input', 'reason', 0) + mockDataNode.getXpath() >> { throw originalException } + when: 'attempt to create data updated event' + objectUnderTest.createCpsDataUpdatedEvent(anchor, OffsetDateTime.now(), Operation.UPDATE) + then: 'the same exception is thrown up' + def thrownUp = thrown(URISyntaxException) + assert thrownUp == originalException + } + def isExpectedDateTimeFormat(String observedTimestamp) { try { DateTimeFormatter.ofPattern(dateTimeFormat).parse(observedTimestamp) diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy index d0cd47383f..89e305aedb 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022 Nordix Foundation + * Copyright (C) 2022-2023 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,15 +44,17 @@ class NotificationErrorHandlerSpec extends Specification{ ((Logger) LoggerFactory.getLogger(NotificationErrorHandler.class)).detachAndStopAllAppenders(); } - def 'Logging exception via notification error handler'() { - when: 'some exception occurs' - objectUnderTest.onException(new Exception('sample exception'), 'some context') + def 'Logging exception via notification error handler #scenario'() { + when: 'exception #scenario occurs' + objectUnderTest.onException(exception, 'some context') then: 'log output results contains the correct error details' - def logMessage = logWatcher.list.get(0).getFormattedMessage() - logMessage.contains( - "Failed to process \n" + - " Error cause: sample exception \n" + - " Error context: [some context]") + def logMessage = logWatcher.list[0].getFormattedMessage() + assert logMessage.contains('Failed to process') + assert logMessage.contains("Error cause: ${exptectedCauseString}") + assert logMessage.contains("Error context: [some context]") + where: + scenario | exception || exptectedCauseString + 'with cause' | new Exception('message') || 'message' + 'without cause' | new Exception('message', new RuntimeException('cause')) || 'java.lang.RuntimeException: cause' } } - diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy index 2ef468bb53..f07f89b391 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy @@ -42,14 +42,14 @@ import java.util.concurrent.TimeUnit @ContextConfiguration(classes = [NotificationProperties, NotificationService, NotificationErrorHandler, AsyncConfig]) class NotificationServiceSpec extends Specification { + @SpringSpy + NotificationProperties spyNotificationProperties @SpringBean NotificationPublisher mockNotificationPublisher = Mock() @SpringBean CpsDataUpdatedEventFactory mockCpsDataUpdatedEventFactory = Mock() @SpringSpy NotificationErrorHandler spyNotificationErrorHandler - @SpringSpy - NotificationProperties spyNotificationProperties @SpringBean CpsAdminService mockCpsAdminService = Mock() @@ -146,4 +146,13 @@ class NotificationServiceSpec extends Specification { notThrown Exception 1 * spyNotificationErrorHandler.onException(_, _, _, '/', Operation.CREATE) } + + def 'Disabled Notification services'() { + given: 'a notification service that is disabled' + spyNotificationProperties.enabled >> false + NotificationService notificationService = new NotificationService(spyNotificationProperties, mockNotificationPublisher, mockCpsDataUpdatedEventFactory, spyNotificationErrorHandler, mockCpsAdminService) + notificationService.init() + expect: 'it will not send notifications' + assert notificationService.shouldSendNotification('') == false + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy index 24f3487d17..c1958472e9 100644 --- a/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy @@ -21,6 +21,7 @@ package org.onap.cps.spi +import org.onap.cps.spi.exceptions.DataValidationException import spock.lang.Specification class FetchDescendantsOptionSpec extends Specification { @@ -74,10 +75,10 @@ class FetchDescendantsOptionSpec extends Specification { thrown IllegalArgumentException } - def 'Create fetch descendant option with descendant using #scenario.'() { - when: 'the next level of depth is not allowed' - def FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString) - then: 'fetch descendant object created' + def 'Create fetch descendant option from string scenario: #scenario.'() { + when: 'create fetch descendant option from string' + def fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString) + then: 'fetch descendant object created with correct depth' assert fetchDescendantsOption.depth == expectedDepth where: 'following parameters are used' scenario | fetchDescendantsOptionAsString || expectedDepth @@ -85,10 +86,21 @@ class FetchDescendantsOptionSpec extends Specification { 'all descendants using all' | 'all' || -1 'No descendants by default' | '' || 0 'No descendants using none' | 'none' || 0 + 'No descendants using number' | '0' || 0 + 'direct child using number' | '1' || 1 + 'direct child using direct' | 'direct' || 1 'til 10th descendants using number' | '10' || 10 } - def 'String values.'() { + def 'Create fetch descendant option from string with invalid string.'() { + when: 'attempt to create fetch descendant option from invalid string' + FetchDescendantsOption.getFetchDescendantsOption('invalid-string') + then: 'a validation exception is thrown with the invalid string in the details' + def thrown = thrown(DataValidationException) + thrown.details.contains('invalid-string') + } + + def 'Convert to string.'() { expect: 'each fetch descendant option has the correct String value' assert fetchDescendantsOption.toString() == expectedStringValue where: 'the following option is used' diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy new file mode 100644 index 0000000000..c8446902d5 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 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.spi.model + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.utils.JsonObjectMapper +import spock.lang.Specification + +class ConditionPropertiesSpec extends Specification { + + ObjectMapper objectMapper = new ObjectMapper() + + def 'Condition Properties JSON conversion.'() { + given: 'a condition properties' + def objectUnderTest = new ConditionProperties(conditionName: 'test', conditionParameters: [ [ key : 'value' ] ]) + expect: 'the name is blank' + assert objectMapper.writeValueAsString(objectUnderTest) == '{"conditionName":"test","conditionParameters":[{"key":"value"}]}' + } + +} 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 1559783e97..fcbae628e6 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-2022 Nordix Foundation. + * Modifications Copyright (C) 2021-2023 Nordix Foundation. * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,15 +22,19 @@ package org.onap.cps.spi.model import org.onap.cps.TestUtils +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.utils.DataMapUtils import org.onap.cps.utils.YangUtils import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode +import org.opendaylight.yangtools.yang.data.api.schema.ForeignDataNode import spock.lang.Specification class DataNodeBuilderSpec extends Specification { - Map<String, Map<String, Serializable>> expectedLeavesByXpathMap = [ + def objectUnderTest = new DataNodeBuilder() + + def expectedLeavesByXpathMap = [ '/test-tree' : [], '/test-tree/branch[@name=\'Left\']' : [name: 'Left'], '/test-tree/branch[@name=\'Left\']/nest' : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']], @@ -56,7 +60,7 @@ class DataNodeBuilderSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('test-tree.json') def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) when: 'the container node is converted to a data node' - def result = new DataNodeBuilder().withContainerNode(containerNode).build() + def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: '6 DataNode objects with unique xpath were created in total' mappedResult.size() == 6 @@ -76,16 +80,12 @@ class DataNodeBuilderSpec extends Specification { def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }' def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree") when: 'the container node is converted to a data node with parent node xpath defined' - def result = new DataNodeBuilder() - .withContainerNode(containerNode) - .withParentNodeXpath("/test-tree") - .build() + def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath('/test-tree').build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: '2 DataNode objects with unique xpath were created in total' mappedResult.size() == 2 and: 'all expected xpaths were built' - mappedResult.keySet() - .containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest']) + mappedResult.keySet().containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest']) } def 'Converting ContainerNode (tree) to a DataNode (tree) -- augmentation case.'() { @@ -96,11 +96,10 @@ class DataNodeBuilderSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json') def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) when: 'the container node is converted to a data node ' - def result = new DataNodeBuilder().withContainerNode(containerNode).build() + def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: 'all expected data nodes are populated' mappedResult.size() == 32 - println(mappedResult.keySet().sort()) and: 'xpaths for augmentation nodes (link and termination-point nodes) were built correctly' mappedResult.keySet().containsAll([ "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']", @@ -130,8 +129,7 @@ class DataNodeBuilderSpec extends Specification { def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}' def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) when: 'the container node is converted to a data node with given parent node xpath' - def result = new DataNodeBuilder().withContainerNode(containerNode) - .withParentNodeXpath(parentNodeXpath).build() + def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).build() then: 'the resulting data node represents a child of augmentation node' assert result.xpath == "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']/source" assert result.leaves['source-node'] == 'D1' @@ -146,15 +144,13 @@ class DataNodeBuilderSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json') def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) when: 'the container node is converted to a data node' - def result = new DataNodeBuilder().withContainerNode(containerNode).build() + def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: 'the resulting data node contains only one xpath with 3 leaves' - mappedResult.keySet().containsAll([ - "/container-with-choice-leaves" - ]) - assert result.leaves['leaf-1'] == "test" - assert result.leaves['choice-case1-leaf-a'] == "test" - assert result.leaves['choice-case1-leaf-b'] == "test" + mappedResult.keySet().containsAll([ '/container-with-choice-leaves' ]) + assert result.leaves['leaf-1'] == 'test' + assert result.leaves['choice-case1-leaf-a'] == 'test' + assert result.leaves['choice-case1-leaf-b'] == 'test' } def 'Converting ContainerNode into DataNode collection: #scenario.'() { @@ -162,12 +158,11 @@ class DataNodeBuilderSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'parent node xpath referencing parent of list element' - def parentNodeXpath = "/test-tree" + def parentNodeXpath = '/test-tree' and: 'the json data fragment (list element) parsed into container node object' def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) when: 'the container node is converted to a data node collection' - def result = new DataNodeBuilder().withContainerNode(containerNode) - .withParentNodeXpath(parentNodeXpath).buildCollection() + def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).buildCollection() def resultXpaths = result.collect { it.getXpath() } then: 'the resulting collection contains data nodes for expected list elements' assert resultXpaths.size() == expectedSize @@ -178,15 +173,43 @@ class DataNodeBuilderSpec extends Specification { 'multiple entries' | '{"branch": [{"name": "One"}, {"name": "Two"}]}' | 2 | ['/test-tree/branch[@name=\'One\']', '/test-tree/branch[@name=\'Two\']'] } - def 'Converting ContainerNode to a DataNode collection -- edge cases: #scenario.'() { - when: 'the container node is #node' - def result = new DataNodeBuilder().withContainerNode(containerNode).buildCollection() - then: 'the resulting collection contains data nodes for expected list elements' - assert result.isEmpty() - where: 'following parameters are used' - scenario | containerNode - 'ContainerNode is null' | null - 'ContainerNode is an unsupported type' | Mock(ContainerNode) + def 'Converting ContainerNode to a Collection with #scenario.'() { + expect: 'converting null to a collection returns an empty collection' + assert objectUnderTest.withContainerNode(containerNode).buildCollection().isEmpty() + where: 'the following container node is used' + scenario | containerNode + 'null object' | null + 'object without body' | Mock(ContainerNode) + } + + def 'Converting ContainerNode to a DataNode with unsupported Normalized Node.'() { + given: 'a container node of an unsupported type' + def mockContainerNode = Mock(ContainerNode) + mockContainerNode.body() >> [ Mock(ForeignDataNode) ] + when: 'attempt to convert it' + objectUnderTest.withContainerNode(mockContainerNode).build() + then: 'a data validation exception is thrown' + thrown(DataValidationException) + } + + def 'Build datanode from attributes.'() { + when: 'data node is built' + def result = new DataNodeBuilder() + .withDataspace('my dataspace') + .withAnchor('my anchor') + .withModuleNamePrefix('my prefix') + .withXpath('some xpath') + .withLeaves([leaf1: 'value1']) + .withChildDataNodes([Mock(DataNode)]) + .build() + then: 'the datanode has all the defined attributes' + assert result.dataspace == 'my dataspace' + assert result.anchorName == 'my anchor' + assert result.moduleNamePrefix == 'my prefix' + assert result.moduleNamePrefix == 'my prefix' + assert result.xpath == 'some xpath' + assert result.leaves == [leaf1: 'value1'] + assert result.childDataNodes.size() == 1 } def 'Use of adding the module name prefix attribute of data node.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy index 2332282e2b..8cbd493550 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy @@ -46,13 +46,23 @@ class JsonObjectMapperSpec extends Specification { type << ['String', 'bytes'] } + def 'Convert to bytes with processing exception.'() { + given: 'the object mapper throws an processing exception' + spiedObjectMapper.writeValueAsBytes(_) >> { throw new JsonProcessingException('message from cause')} + when: 'attempt to convert an object to bytes' + jsonObjectMapper.asJsonBytes('does not matter') + then: 'a data validation exception is thrown with the original exception message as details' + def thrown = thrown(DataValidationException) + assert thrown.details == 'message from cause' + } + def 'Map a structured object to json String error.'() { given: 'some object' def object = new Object() and: 'the Object mapper throws an exception' spiedObjectMapper.writeValueAsString(object) >> { throw new JsonProcessingException('Sample problem'){} } when: 'attempting to convert the object to a string' - jsonObjectMapper.asJsonString(object); + jsonObjectMapper.asJsonString(object) then: 'a Data Validation Exception is thrown' def thrown = thrown(DataValidationException) and: 'the details containing the original error message' @@ -63,21 +73,27 @@ class JsonObjectMapperSpec extends Specification { given: 'a map object model' def contentMap = new JsonSlurper().parseText(TestUtils.getResourceFileContent('bookstore.json')) when: 'converted into a Map' - def result = jsonObjectMapper.convertToValueType(contentMap, Map); + def result = jsonObjectMapper.convertToValueType(contentMap, Map) then: 'the result is a mapped into class of type Map' assert result instanceof Map and: 'the map contains the expected key' assert result.containsKey('test:bookstore') assert result.'test:bookstore'.categories[0].name == 'SciFi' + } + def 'Mapping a valid json string to class object of specific class type T.'() { + given: 'a json string representing a map' + def content = '{"key":"value"}' + expect: 'the string is converted correctly to a map' + jsonObjectMapper.convertJsonString(content, Map) == [ key: 'value' ] } def 'Mapping an unstructured json string to class object of specific class type T.'() { given: 'Unstructured json string' - def content = '{ "nest": { "birds": "bird"] } }' + def content = '{invalid json' when: 'mapping json string to given class type' - jsonObjectMapper.convertJsonString(content, Map); - then: 'an exception is thrown' + jsonObjectMapper.convertJsonString(content, Map) + then: 'a data validation exception is thrown' thrown(DataValidationException) } @@ -87,7 +103,7 @@ class JsonObjectMapperSpec extends Specification { and: 'Object mapper throws an exception' spiedObjectMapper.convertValue(*_) >> { throw new IllegalArgumentException() } when: 'converted into specific class type' - jsonObjectMapper.convertToValueType(contentMap, Object); + jsonObjectMapper.convertToValueType(contentMap, Object) then: 'an exception is thrown' thrown(DataValidationException) } @@ -96,9 +112,9 @@ class JsonObjectMapperSpec extends Specification { given: 'Unstructured object' def object = new Object() and: 'disable serialization failure on empty bean' - spiedObjectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + spiedObjectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) when: 'the object is mapped to string' - jsonObjectMapper.asJsonString(object); + jsonObjectMapper.asJsonString(object) then: 'no exception is thrown' noExceptionThrown() } @@ -107,16 +123,16 @@ class JsonObjectMapperSpec extends Specification { given: 'Unstructured object' def content = '{ "nest": { "birds": "bird" } }' when: 'the object is mapped to string' - def result = jsonObjectMapper.convertToJsonNode(content); + def result = jsonObjectMapper.convertToJsonNode(content) then: 'the result is a valid JsonNode' - result.fieldNames().next() == "nest" + result.fieldNames().next() == 'nest' } def 'Map a unstructured json String to JsonNode.'() { given: 'Unstructured object' def content = '{ "nest": { "birds": "bird" }] }' when: 'the object is mapped to string' - jsonObjectMapper.convertToJsonNode(content); + jsonObjectMapper.convertToJsonNode(content) then: 'a data validation exception is thrown' thrown(DataValidationException) } 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 b044e2e727..3864a5253a 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,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG + * Modifications Copyright (c) 2023 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,16 +22,18 @@ package org.onap.cps.utils import org.onap.cps.TestUtils import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.xml.sax.SAXParseException import spock.lang.Specification class XmlFileUtilsSpec extends Specification { + def 'Parse a valid xml content #scenario'(){ given: 'YANG model schema context' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'the XML data is parsed' + when: 'the xml data is parsed' def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext) - then: 'the result XML is wrapped by root node defined in YANG schema' + then: 'the result xml is wrapped by root node defined in YANG schema' assert parsedXmlContent == expectedOutput where: scenario | xmlData || expectedOutput @@ -39,13 +42,22 @@ class XmlFileUtilsSpec extends Specification { 'no xml header' | '<stores><class> </class></stores>' || '<stores><class> </class></stores>' } + def 'Parse a invalid xml content'(){ + given: 'YANG model schema context' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'attempt to parse invalid xml' + XmlFileUtils.prepareXmlContent('invalid-xml', schemaContext) + then: 'a Sax Parser exception is thrown' + thrown(SAXParseException) + } + def 'Parse a xml content with XPath container #scenario'() { given: 'YANG model schema context' 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 = YangUtils.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' @@ -54,8 +66,6 @@ class XmlFileUtilsSpec extends Specification { scenario | xmlData | xPath || expectedOutput 'XML element test tree' | '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' | '/test-tree' || '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' 'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><nest xmlns="org:onap:cps:test:test-tree"><name>Small</name><birds>Sparrow</birds></nest>' | '/test-tree/branch[@name=\'Branch\']' || '<?xml version="1.0" encoding="UTF-8"?><branch xmlns="org:onap:cps:test:test-tree"><name>Branch</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>' - - } } 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 50b6306439..e6344d3035 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-2022 Nordix Foundation + * Copyright (C) 2020-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG @@ -27,6 +27,7 @@ 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 @@ -162,4 +163,12 @@ class YangUtilsSpec extends Specification { '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' + def mockPathArgument = Mock(YangInstanceIdentifier.NodeIdentifierWithPredicates) + mockPathArgument.entrySet() >> [ ] + expect: 'the result is an empty string' + YangUtils.getKeyAttributesStatement(mockPathArgument) == '' + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy index 3b4d57d3a6..2739281bc7 100644 --- a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy @@ -23,13 +23,13 @@ package org.onap.cps.yang - import org.onap.cps.TestUtils import org.onap.cps.spi.exceptions.ModelValidationException -import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.common.Revision import spock.lang.Specification +import java.nio.charset.StandardCharsets + class YangTextSchemaSourceSetBuilderSpec extends Specification { def 'Building a valid YangTextSchemaSourceSet using #filenameCase filename.'() { @@ -62,4 +62,16 @@ class YangTextSchemaSourceSetBuilderSpec extends Specification { 'invalid-empty.yang' | 'no valid content' || ModelValidationException 'invalid-missing-import.yang' | 'no dependency module' || ModelValidationException } + + def 'Convert yang source to a YangTextSchemaSource.'() { + given: 'a yang source text' + def yangSourceText = TestUtils.getResourceFileContent('bookstore.yang') + when: 'convert it to a YangTextSchemaSource' + def result = YangTextSchemaSourceSetBuilder.toYangTextSchemaSource('some name', yangSourceText) + then: 'the converted object has correct properties' + assert result.toString() == '{identifier=RevisionSourceIdentifier [name=some name]}' + assert new String(result.openStream().readAllBytes(), StandardCharsets.UTF_8) == yangSourceText + and: 'it has no symbolic name' + assert result.getSymbolicName().isEmpty() + } } |