From 67973be1deea7de52b750e9bdd6dc53da265da65 Mon Sep 17 00:00:00 2001 From: "rajesh.kumar" Date: Mon, 30 Sep 2024 18:11:47 +0530 Subject: Add schema to persist notification subscription information Add required schema to persist notification subscription information. It should contain - Schema yang file - New Dataspace, Anchors or any other database entity - Refactore duplicate code in NCMP Issue-ID:CPS-2427 Change-Id: I56c34400dc73c71b936a51260efd300924ababdc Signed-off-by: rajesh.kumar --- .../onap/cps/ncmp/init/AbstractModelLoader.java | 157 ------------- .../ncmp/init/CmDataSubscriptionModelLoader.java | 1 + .../onap/cps/ncmp/init/InventoryModelLoader.java | 1 + .../java/org/onap/cps/ncmp/init/ModelLoader.java | 34 --- .../cps/ncmp/init/AbstractModelLoaderSpec.groovy | 227 ------------------ .../init/CmDataSubscriptionModelLoaderSpec.groovy | 2 +- .../cps/ncmp/init/InventoryModelLoaderSpec.groovy | 2 +- .../api/exceptions/ModelOnboardingException.java | 28 +++ .../org/onap/cps/init/AbstractModelLoader.java | 183 +++++++++++++++ .../CpsNotificationSubscriptionModelLoader.java | 60 +++++ .../main/java/org/onap/cps/init/ModelLoader.java | 32 +++ .../cps-notification-subscriptions@2024-07-03.yang | 48 ++++ .../onap/cps/init/AbstractModelLoaderSpec.groovy | 253 +++++++++++++++++++++ ...sNotificationSubscriptionModelLoaderSpec.groovy | 87 +++++++ 14 files changed, 695 insertions(+), 420 deletions(-) delete mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java delete mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy create mode 100644 cps-service/src/main/java/org/onap/cps/api/exceptions/ModelOnboardingException.java create mode 100644 cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java create mode 100644 cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java create mode 100644 cps-service/src/main/java/org/onap/cps/init/ModelLoader.java create mode 100644 cps-service/src/main/resources/models/cps-notification-subscriptions@2024-07-03.yang create mode 100644 cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy create mode 100644 cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java deleted file mode 100644 index ad5e431944..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2023-2024 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.ncmp.init; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.time.OffsetDateTime; -import java.util.HashMap; -import java.util.Map; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.onap.cps.api.CpsAnchorService; -import org.onap.cps.api.CpsDataService; -import org.onap.cps.api.CpsDataspaceService; -import org.onap.cps.api.CpsModuleService; -import org.onap.cps.api.exceptions.AlreadyDefinedException; -import org.onap.cps.api.parameters.CascadeDeleteAllowed; -import org.onap.cps.ncmp.exceptions.NcmpStartUpException; -import org.onap.cps.utils.JsonObjectMapper; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.context.event.ApplicationStartedEvent; - -@Slf4j -@RequiredArgsConstructor -abstract class AbstractModelLoader implements ModelLoader { - - private final CpsDataspaceService cpsDataspaceService; - private final CpsModuleService cpsModuleService; - private final CpsAnchorService cpsAnchorService; - protected final CpsDataService cpsDataService; - - private static final int EXIT_CODE_ON_ERROR = 1; - - private final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()); - - @Value("${ncmp.model-loader.maximum-attempt-count:20}") - int maximumAttemptCount; - - @Value("${ncmp.timers.model-loader.retry-time-ms:1000}") - long retryTimeMs; - - @Override - public void onApplicationEvent(@NonNull final ApplicationStartedEvent applicationStartedEvent) { - try { - onboardOrUpgradeModel(); - } catch (final NcmpStartUpException ncmpStartUpException) { - log.error("Onboarding model for NCMP failed: {} ", ncmpStartUpException.getMessage()); - SpringApplication.exit(applicationStartedEvent.getApplicationContext(), () -> EXIT_CODE_ON_ERROR); - } - } - - void createSchemaSet(final String dataspaceName, final String schemaSetName, final String... resourceNames) { - try { - final Map yangResourcesContentMap = createYangResourcesToContentMap(resourceNames); - cpsModuleService.createSchemaSet(dataspaceName, schemaSetName, yangResourcesContentMap); - } catch (final AlreadyDefinedException alreadyDefinedException) { - log.warn("Creating new schema set failed as schema set already exists"); - } catch (final Exception exception) { - log.error("Creating schema set failed: {} ", exception.getMessage()); - throw new NcmpStartUpException("Creating schema set failed", exception.getMessage()); - } - } - - void createDataspace(final String dataspaceName) { - try { - cpsDataspaceService.createDataspace(dataspaceName); - } catch (final AlreadyDefinedException alreadyDefinedException) { - log.debug("Dataspace already exists"); - } catch (final Exception exception) { - log.error("Creating dataspace failed: {} ", exception.getMessage()); - throw new NcmpStartUpException("Creating dataspace failed", exception.getMessage()); - } - } - - void deleteUnusedSchemaSets(final String dataspaceName, final String... schemaSetNames) { - for (final String schemaSetName : schemaSetNames) { - try { - cpsModuleService.deleteSchemaSet( - dataspaceName, schemaSetName, CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED); - } catch (final Exception exception) { - log.warn("Deleting schema set failed: {} ", exception.getMessage()); - } - } - } - - void createAnchor(final String dataspaceName, final String schemaSetName, final String anchorName) { - try { - cpsAnchorService.createAnchor(dataspaceName, schemaSetName, anchorName); - } catch (final AlreadyDefinedException alreadyDefinedException) { - log.warn("Creating new anchor failed as anchor already exists"); - } catch (final Exception exception) { - log.error("Creating anchor failed: {} ", exception.getMessage()); - throw new NcmpStartUpException("Creating anchor failed", exception.getMessage()); - } - } - - void createTopLevelDataNode(final String dataspaceName, final String anchorName, final String dataNodeName) { - final String nodeData = jsonObjectMapper.asJsonString(Map.of(dataNodeName, Map.of())); - try { - cpsDataService.saveData(dataspaceName, anchorName, nodeData, OffsetDateTime.now()); - } catch (final AlreadyDefinedException exception) { - log.warn("Creating new data node '{}' failed as data node already exists", dataNodeName); - } catch (final Exception exception) { - log.error("Creating data node failed: {}", exception.getMessage()); - throw new NcmpStartUpException("Creating data node failed", exception.getMessage()); - } - } - - void updateAnchorSchemaSet(final String dataspaceName, final String anchorName, final String schemaSetName) { - try { - cpsAnchorService.updateAnchorSchemaSet(dataspaceName, anchorName, schemaSetName); - } catch (final Exception exception) { - log.error("Updating schema set failed: {}", exception.getMessage()); - throw new NcmpStartUpException("Updating schema set failed", exception.getMessage()); - } - } - - Map createYangResourcesToContentMap(final String... resourceNames) { - final Map yangResourcesToContentMap = new HashMap<>(); - for (final String resourceName: resourceNames) { - yangResourcesToContentMap.put(resourceName, getFileContentAsString("models/" + resourceName)); - } - return yangResourcesToContentMap; - } - - private String getFileContentAsString(final String fileName) { - try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName)) { - return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - } catch (final Exception exception) { - final String message = String.format("Onboarding failed as unable to read file: %s", fileName); - log.debug(message); - throw new NcmpStartUpException(message, exception.getMessage()); - } - } - -} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoader.java index d1145fd24a..d5238ae06b 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoader.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoader.java @@ -30,6 +30,7 @@ import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsDataspaceService; import org.onap.cps.api.CpsModuleService; import org.onap.cps.api.exceptions.AlreadyDefinedException; +import org.onap.cps.init.AbstractModelLoader; import org.onap.cps.ncmp.api.data.models.DatastoreType; import org.onap.cps.ncmp.exceptions.NcmpStartUpException; import org.springframework.stereotype.Service; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java index 76d12f290c..58a5f553af 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java @@ -29,6 +29,7 @@ import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsDataspaceService; import org.onap.cps.api.CpsModuleService; +import org.onap.cps.init.AbstractModelLoader; import org.springframework.stereotype.Service; @Slf4j diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java deleted file mode 100644 index 9832ba3f9e..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2023-2024 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.ncmp.init; - -import lombok.NonNull; -import org.springframework.boot.context.event.ApplicationStartedEvent; -import org.springframework.context.ApplicationListener; - -public interface ModelLoader extends ApplicationListener { - - @Override - void onApplicationEvent(@NonNull ApplicationStartedEvent applicationStartedEvent); - - void onboardOrUpgradeModel(); - -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy deleted file mode 100644 index 63192e40c6..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy +++ /dev/null @@ -1,227 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2023-2024 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.ncmp.init - -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import ch.qos.logback.core.read.ListAppender -import org.onap.cps.api.CpsAnchorService -import org.onap.cps.api.CpsDataService -import org.onap.cps.api.CpsDataspaceService -import org.onap.cps.api.CpsModuleService -import org.onap.cps.ncmp.exceptions.NcmpStartUpException -import org.onap.cps.api.parameters.CascadeDeleteAllowed -import org.onap.cps.api.exceptions.AlreadyDefinedException -import org.slf4j.LoggerFactory -import org.springframework.boot.SpringApplication -import org.springframework.boot.context.event.ApplicationStartedEvent -import org.springframework.context.annotation.AnnotationConfigApplicationContext -import spock.lang.Specification - -class AbstractModelLoaderSpec extends Specification { - - def mockCpsDataspaceService = Mock(CpsDataspaceService) - def mockCpsModuleService = Mock(CpsModuleService) - def mockCpsDataService = Mock(CpsDataService) - def mockCpsAnchorService = Mock(CpsAnchorService) - def objectUnderTest = Spy(new TestModelLoader(mockCpsDataspaceService, mockCpsModuleService, mockCpsAnchorService, mockCpsDataService)) - - def applicationContext = new AnnotationConfigApplicationContext() - - def yangResourceToContentMap - def logger = (Logger) LoggerFactory.getLogger(AbstractModelLoader) - def loggingListAppender - - void setup() { - yangResourceToContentMap = objectUnderTest.createYangResourcesToContentMap('cm-data-subscriptions@2024-02-12.yang') - logger.setLevel(Level.DEBUG) - loggingListAppender = new ListAppender() - logger.addAppender(loggingListAppender) - loggingListAppender.start() - applicationContext.refresh() - } - - void cleanup() { - ((Logger) LoggerFactory.getLogger(CmDataSubscriptionModelLoader.class)).detachAndStopAllAppenders() - applicationContext.close() - } - - def 'Application started event'() { - when: 'Application (started) event is triggered' - objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent)) - then: 'the onboard/upgrade method is executed' - 1 * objectUnderTest.onboardOrUpgradeModel() - } - - def 'Application started event with start up exception'() { - given: 'a start up exception is thrown doing model onboarding' - objectUnderTest.onboardOrUpgradeModel() >> { throw new NcmpStartUpException('test message','details are not logged') } - when: 'Application (started) event is triggered' - objectUnderTest.onApplicationEvent(new ApplicationStartedEvent(new SpringApplication(), null, applicationContext, null)) - then: 'the exception message is logged' - def logs = loggingListAppender.list.toString() - assert logs.contains('test message') - } - - def 'Create schema set.'() { - when: 'creating a schema set' - objectUnderTest.createSchemaSet('some dataspace','new name','cm-data-subscriptions@2024-02-12.yang') - then: 'the operation is delegated to the admin service' - 1 * mockCpsModuleService.createSchemaSet('some dataspace','new name',_) - } - - def 'Create schema set with already defined exception.'() { - given: 'the module service throws an already defined exception' - mockCpsModuleService.createSchemaSet(*_) >> { throw AlreadyDefinedException.forSchemaSet('name','context',null) } - when: 'attempt to create a schema set' - objectUnderTest.createSchemaSet('some dataspace','new name','cm-data-subscriptions@2024-02-12.yang') - then: 'the exception is ignored i.e. no exception thrown up' - noExceptionThrown() - and: 'the exception message is logged' - def logs = loggingListAppender.list.toString() - assert logs.contains('Creating new schema set failed as schema set already exists') - } - - def 'Create schema set with non existing yang file.'() { - when: 'attempt to create a schema set from a non existing file' - objectUnderTest.createSchemaSet('some dataspace','some name','no such yang file') - then: 'a startup exception with correct message and details is thrown' - def thrown = thrown(NcmpStartUpException) - assert thrown.message.contains('Creating schema set failed') - assert thrown.details.contains('unable to read file') - } - - def 'Delete unused schema sets.'() { - when: 'several unused schemas are deleted ' - objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2') - then: 'a request to delete each (without cascade) is delegated to the module service' - 1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 1', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED) - 1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED) - - } - - def 'Delete unused schema sets with exception.'() { - given: 'deleting the first schemaset causes an exception' - mockCpsModuleService.deleteSchemaSet(_, 'schema set 1', _) >> { throw new RuntimeException('test message')} - when: 'several unused schemas are deleted ' - objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2') - then: 'the exception message is logged' - def logs = loggingListAppender.list.toString() - assert logs.contains('Deleting schema set failed') - assert logs.contains('test message') - and: 'the second schema set is still deleted' - 1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED) - } - - def 'Create anchor.'() { - when: 'creating an anchor' - objectUnderTest.createAnchor('some dataspace','some schema set','new name') - then: 'the operation is delegated to the admin service' - 1 * mockCpsAnchorService.createAnchor('some dataspace','some schema set', 'new name') - } - - def 'Create anchor with already defined exception.'() { - given: 'the admin service throws an already defined exception' - mockCpsAnchorService.createAnchor(*_)>> { throw AlreadyDefinedException.forAnchor('name','context',null) } - when: 'attempt to create anchor' - objectUnderTest.createAnchor('some dataspace','some schema set','new name') - then: 'the exception is ignored i.e. no exception thrown up' - noExceptionThrown() - and: 'the exception message is logged' - def logs = loggingListAppender.list.toString() - assert logs.contains('Creating new anchor failed as anchor already exists') - } - - def 'Create anchor with any other exception.'() { - given: 'the admin service throws a exception' - mockCpsAnchorService.createAnchor(*_)>> { throw new RuntimeException('test message') } - when: 'attempt to create anchor' - objectUnderTest.createAnchor('some dataspace','some schema set','new name') - then: 'a startup exception with correct message and details is thrown' - def thrown = thrown(NcmpStartUpException) - assert thrown.message.contains('Creating anchor failed') - assert thrown.details.contains('test message') - } - - def 'Create top level node.'() { - when: 'top level node is created' - objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node') - then: 'the correct json is saved using the data service' - 1 * mockCpsDataService.saveData('dataspace','anchor', '{"new node":{}}',_) - } - - def 'Create top level node with already defined exception.'() { - given: 'the data service throws an Already Defined exception' - mockCpsDataService.saveData(*_) >> { throw AlreadyDefinedException.forDataNodes([], 'some context') } - when: 'attempt to create top level node' - objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node') - then: 'the exception is ignored i.e. no exception thrown up' - noExceptionThrown() - and: 'the exception message is logged' - def logs = loggingListAppender.list.toString() - assert logs.contains('failed as data node already exists') - } - - def 'Create top level node with any other exception.'() { - given: 'the data service throws an exception' - mockCpsDataService.saveData(*_) >> { throw new RuntimeException('test message') } - when: 'attempt to create top level node' - objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node') - then: 'a startup exception with correct message and details is thrown' - def thrown = thrown(NcmpStartUpException) - assert thrown.message.contains('Creating data node failed') - assert thrown.details.contains('test message') - } - - def 'Update anchor schema set.'() { - when: 'a schema set for an anchor is updated' - objectUnderTest.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set') - then: 'the request is delegated to the admin service' - 1 * mockCpsAnchorService.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set') - } - - def 'Update anchor schema set with exception.'() { - given: 'the admin service throws an exception' - mockCpsAnchorService.updateAnchorSchemaSet(*_) >> { throw new RuntimeException('test message') } - when: 'a schema set for an anchor is updated' - objectUnderTest.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set') - then: 'a startup exception with correct message and details is thrown' - def thrown = thrown(NcmpStartUpException) - assert thrown.message.contains('Updating schema set failed') - assert thrown.details.contains('test message') - } - - class TestModelLoader extends AbstractModelLoader { - - TestModelLoader(final CpsDataspaceService cpsDataspaceService, - final CpsModuleService cpsModuleService, - final CpsAnchorService cpsAnchorService, - final CpsDataService cpsDataService) { - super(cpsDataspaceService, cpsModuleService, cpsAnchorService, cpsDataService) - super.maximumAttemptCount = 2 - super.retryTimeMs = 1 - } - - @Override - void onboardOrUpgradeModel() { } - } - -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy index 21f5e98e10..926d00041f 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy @@ -52,7 +52,7 @@ class CmDataSubscriptionModelLoaderSpec extends Specification { def loggingListAppender void setup() { - expectedYangResourcesToContentMap = objectUnderTest.createYangResourcesToContentMap('cm-data-subscriptions@2024-02-12.yang') + expectedYangResourcesToContentMap = objectUnderTest.mapYangResourcesToContent('cm-data-subscriptions@2024-02-12.yang') logger.setLevel(Level.DEBUG) loggingListAppender = new ListAppender() logger.addAppender(loggingListAppender) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy index b37cfbaab5..ffd1d89fe1 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy @@ -51,7 +51,7 @@ class InventoryModelLoaderSpec extends Specification { def loggingListAppender void setup() { - expectedYangResourceToContentMap = objectUnderTest.createYangResourcesToContentMap('dmi-registry@2024-02-23.yang') + expectedYangResourceToContentMap = objectUnderTest.mapYangResourcesToContent('dmi-registry@2024-02-23.yang') logger.setLevel(Level.DEBUG) loggingListAppender = new ListAppender() logger.addAppender(loggingListAppender) diff --git a/cps-service/src/main/java/org/onap/cps/api/exceptions/ModelOnboardingException.java b/cps-service/src/main/java/org/onap/cps/api/exceptions/ModelOnboardingException.java new file mode 100644 index 0000000000..d4455ebc20 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/api/exceptions/ModelOnboardingException.java @@ -0,0 +1,28 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 TechMahindra Ltd. + * ================================================================================ + * 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.api.exceptions; + +public class ModelOnboardingException extends CpsException { + + public ModelOnboardingException(final String message, final String details) { + super(message, details); + } +} diff --git a/cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java b/cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java new file mode 100644 index 0000000000..e864633f25 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java @@ -0,0 +1,183 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023-2024 Nordix Foundation + * Modifications Copyright (C) 2024 TechMahindra Ltd. + * ================================================================================ + * 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.init; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.api.CpsAnchorService; +import org.onap.cps.api.CpsDataService; +import org.onap.cps.api.CpsDataspaceService; +import org.onap.cps.api.CpsModuleService; +import org.onap.cps.api.exceptions.AlreadyDefinedException; +import org.onap.cps.api.exceptions.ModelOnboardingException; +import org.onap.cps.api.parameters.CascadeDeleteAllowed; +import org.onap.cps.utils.JsonObjectMapper; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.event.ApplicationStartedEvent; + +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractModelLoader implements ModelLoader { + + protected final CpsDataspaceService cpsDataspaceService; + private final CpsModuleService cpsModuleService; + private final CpsAnchorService cpsAnchorService; + protected final CpsDataService cpsDataService; + + private final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()); + + private static final int EXIT_CODE_ON_ERROR = 1; + + @Override + public void onApplicationEvent(final ApplicationStartedEvent applicationStartedEvent) { + try { + onboardOrUpgradeModel(); + } catch (final Exception modelOnboardUpException) { + log.error("Exiting application due to failure in onboarding model: {} ", + modelOnboardUpException.getMessage()); + SpringApplication.exit(applicationStartedEvent.getApplicationContext(), () -> EXIT_CODE_ON_ERROR); + } + } + + /** + * Create initial schema set. + * @param dataspaceName dataspace name + * @param schemaSetName schemaset name + * @param resourceNames resource names + */ + public void createSchemaSet(final String dataspaceName, final String schemaSetName, final String... resourceNames) { + try { + final Map yangResourcesContentByResourceName = mapYangResourcesToContent(resourceNames); + cpsModuleService.createSchemaSet(dataspaceName, schemaSetName, yangResourcesContentByResourceName); + } catch (final AlreadyDefinedException alreadyDefinedException) { + log.warn("Creating new schema set failed as schema set already exists"); + } catch (final Exception exception) { + log.error("Creating schema set failed: {} ", exception.getMessage()); + throw new ModelOnboardingException("Creating schema set failed", exception.getMessage()); + } + } + + /** + * Create initial dataspace. + * @param dataspaceName dataspace name + */ + public void createDataspace(final String dataspaceName) { + try { + cpsDataspaceService.createDataspace(dataspaceName); + } catch (final AlreadyDefinedException alreadyDefinedException) { + log.debug("Dataspace already exists"); + } catch (final Exception exception) { + log.error("Creating dataspace failed: {} ", exception.getMessage()); + throw new ModelOnboardingException("Creating dataspace failed", exception.getMessage()); + } + } + + /** + * Create initial anchor. + * @param dataspaceName dataspace name + * @param schemaSetName schemaset name + * @param anchorName anchor name + */ + public void createAnchor(final String dataspaceName, final String schemaSetName, final String anchorName) { + try { + cpsAnchorService.createAnchor(dataspaceName, schemaSetName, anchorName); + } catch (final AlreadyDefinedException alreadyDefinedException) { + log.warn("Creating new anchor failed as anchor already exists"); + } catch (final Exception exception) { + log.error("Creating anchor failed: {} ", exception.getMessage()); + throw new ModelOnboardingException("Creating anchor failed", exception.getMessage()); + } + } + + /** + * Create initial top level data node. + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param dataNodeName data node name + */ + public void createTopLevelDataNode(final String dataspaceName, final String anchorName, final String dataNodeName) { + final String nodeData = jsonObjectMapper.asJsonString(Map.of(dataNodeName, Map.of())); + try { + cpsDataService.saveData(dataspaceName, anchorName, nodeData, OffsetDateTime.now()); + } catch (final AlreadyDefinedException exception) { + log.warn("Creating new data node '{}' failed as data node already exists", dataNodeName); + } catch (final Exception exception) { + log.error("Creating data node failed: {}", exception.getMessage()); + throw new ModelOnboardingException("Creating data node failed", exception.getMessage()); + } + } + + /** + * Delete unused schema set. + * @param dataspaceName dataspace name + * @param schemaSetNames schema set names + */ + public void deleteUnusedSchemaSets(final String dataspaceName, final String... schemaSetNames) { + for (final String schemaSetName : schemaSetNames) { + try { + cpsModuleService.deleteSchemaSet( + dataspaceName, schemaSetName, CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED); + } catch (final Exception exception) { + log.warn("Deleting schema set failed: {} ", exception.getMessage()); + } + } + } + + /** + * Update anchor schema set. + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param schemaSetName schemaset name + */ + public void updateAnchorSchemaSet(final String dataspaceName, final String anchorName, final String schemaSetName) { + try { + cpsAnchorService.updateAnchorSchemaSet(dataspaceName, anchorName, schemaSetName); + } catch (final Exception exception) { + log.error("Updating schema set failed: {}", exception.getMessage()); + throw new ModelOnboardingException("Updating schema set failed", exception.getMessage()); + } + } + + Map mapYangResourcesToContent(final String... resourceNames) { + final Map yangResourceContentByName = new HashMap<>(); + for (final String resourceName: resourceNames) { + yangResourceContentByName.put(resourceName, getFileContentAsString("models/" + resourceName)); + } + return yangResourceContentByName; + } + + private String getFileContentAsString(final String fileName) { + try (final InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName)) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (final Exception exception) { + final String message = String.format("Onboarding failed as unable to read file: %s", fileName); + log.debug(message); + throw new ModelOnboardingException(message, exception.getMessage()); + } + } +} diff --git a/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java b/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java new file mode 100644 index 0000000000..0b7d1609ff --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java @@ -0,0 +1,60 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 TechMahindra Ltd. + * ================================================================================ + * 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.init; + +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.api.CpsAnchorService; +import org.onap.cps.api.CpsDataService; +import org.onap.cps.api.CpsDataspaceService; +import org.onap.cps.api.CpsModuleService; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class CpsNotificationSubscriptionModelLoader extends AbstractModelLoader { + + private static final String MODEL_FILENAME = "cps-notification-subscriptions@2024-07-03.yang"; + private static final String SCHEMASET_NAME = "cps-notification-subscriptions"; + private static final String ANCHOR_NAME = "cps-notification-subscriptions"; + private static final String CPS_DATASPACE_NAME = "CPS-Admin"; + private static final String REGISTRY_DATANODE_NAME = "dataspaces"; + + public CpsNotificationSubscriptionModelLoader(final CpsDataspaceService cpsDataspaceService, + final CpsModuleService cpsModuleService, + final CpsAnchorService cpsAnchorService, + final CpsDataService cpsDataService) { + super(cpsDataspaceService, cpsModuleService, cpsAnchorService, cpsDataService); + } + + @Override + public void onboardOrUpgradeModel() { + onboardSubscriptionModels(); + log.info("Subscription models onboarded successfully"); + } + + private void onboardSubscriptionModels() { + createDataspace(CPS_DATASPACE_NAME); + createSchemaSet(CPS_DATASPACE_NAME, SCHEMASET_NAME, MODEL_FILENAME); + createAnchor(CPS_DATASPACE_NAME, SCHEMASET_NAME, ANCHOR_NAME); + createTopLevelDataNode(CPS_DATASPACE_NAME, ANCHOR_NAME, REGISTRY_DATANODE_NAME); + } + +} diff --git a/cps-service/src/main/java/org/onap/cps/init/ModelLoader.java b/cps-service/src/main/java/org/onap/cps/init/ModelLoader.java new file mode 100644 index 0000000000..89c18b3159 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/init/ModelLoader.java @@ -0,0 +1,32 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 TechMahindra Ltd. + * ================================================================================ + * 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.init; + +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationListener; + +public interface ModelLoader extends ApplicationListener { + + @Override + void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent); + + void onboardOrUpgradeModel(); +} diff --git a/cps-service/src/main/resources/models/cps-notification-subscriptions@2024-07-03.yang b/cps-service/src/main/resources/models/cps-notification-subscriptions@2024-07-03.yang new file mode 100644 index 0000000000..1cab7923ea --- /dev/null +++ b/cps-service/src/main/resources/models/cps-notification-subscriptions@2024-07-03.yang @@ -0,0 +1,48 @@ +module cps-notification-subscriptions { + yang-version 1.1; + namespace "org:onap:cps"; + + prefix cps-notification-subscriptions; + + revision "2024-08-05" { + description + "First release of cps notification subscriptions model"; + } + container dataspaces { + + list dataspace { + key "name"; + + leaf name { + type string; + } + + container anchors { + + list anchor { + key "name"; + + leaf name { + type string; + } + + container xpaths { + + list xpath { + key "path"; + leaf path { + type string; + } + } + } + } + } + leaf-list subscriptionIds { + type string; + } + leaf topic { + type string; + } + } + } +} \ No newline at end of file diff --git a/cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy new file mode 100644 index 0000000000..0618cad951 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy @@ -0,0 +1,253 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023-2024 Nordix Foundation + * Modification Copyright (C) 2024 TechMahindra Ltd. + * ================================================================================ + * 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.init + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsDataspaceService +import org.onap.cps.api.CpsModuleService +import org.onap.cps.api.exceptions.ModelOnboardingException +import org.onap.cps.api.parameters.CascadeDeleteAllowed +import org.onap.cps.api.exceptions.AlreadyDefinedException +import org.slf4j.LoggerFactory +import org.springframework.boot.SpringApplication +import org.springframework.boot.context.event.ApplicationStartedEvent +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Specification + +class AbstractModelLoaderSpec extends Specification { + + def mockCpsDataspaceService = Mock(CpsDataspaceService) + def mockCpsModuleService = Mock(CpsModuleService) + def mockCpsDataService = Mock(CpsDataService) + def mockCpsAnchorService = Mock(CpsAnchorService) + def objectUnderTest = Spy(new TestModelLoader(mockCpsDataspaceService, mockCpsModuleService, mockCpsAnchorService, mockCpsDataService)) + + def applicationContext = new AnnotationConfigApplicationContext() + + def logger = (Logger) LoggerFactory.getLogger(AbstractModelLoader) + def loggingListAppender + + void setup() { + logger.setLevel(Level.DEBUG) + loggingListAppender = new ListAppender() + logger.addAppender(loggingListAppender) + loggingListAppender.start() + applicationContext.refresh() + } + + void cleanup() { + logger.detachAndStopAllAppenders() + applicationContext.close() + loggingListAppender.stop() + } + + def 'Application started event triggers onboarding/upgrade'() { + when: 'Application (started) event is triggered' + objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent)) + then: 'the onboard/upgrade method is executed' + 1 * objectUnderTest.onboardOrUpgradeModel() + } + + def 'Application started event handles startup exception'() { + given: 'a startup exception is thrown during model onboarding' + objectUnderTest.onboardOrUpgradeModel() >> { throw new ModelOnboardingException('test message','details are not logged') } + when: 'Application (started) event is triggered' + objectUnderTest.onApplicationEvent(new ApplicationStartedEvent(new SpringApplication(), null, applicationContext, null)) + then: 'the exception message is logged' + def logs = loggingListAppender.list.toString() + assert logs.contains('test message') + } + + def 'Creating a dataspace delegates to the service.'() { + when: 'creating a dataspace' + objectUnderTest.createDataspace('some dataspace') + then: 'the operation is delegated to the dataspace service' + 1 * mockCpsDataspaceService.createDataspace('some dataspace') + } + + def 'Creating a dataspace handles already defined exception.'() { + given: 'dataspace service throws an already defined exception' + mockCpsDataspaceService.createDataspace(*_) >> { throw AlreadyDefinedException.forDataNodes([], 'some context') } + when: 'creating a dataspace' + objectUnderTest.createDataspace('some dataspace') + then: 'the exception is ignored i.e. no exception thrown up' + noExceptionThrown() + and: 'the exception is ignored, and a log message is produced' + assertLogContains('Dataspace already exists') + } + + def 'Creating a dataspace handles other exception.'() { + given: 'dataspace service throws a runtime exception' + mockCpsDataspaceService.createDataspace(*_) >> { throw new RuntimeException('test message') } + when: 'creating a dataspace' + objectUnderTest.createDataspace('some dataspace') + then: 'a startup exception with correct message and details is thrown' + def thrown = thrown(ModelOnboardingException) + assert thrown.message.contains('Creating dataspace failed') + assert thrown.details.contains('test message') + } + + def 'Creating a schema set delegates to the service.'() { + when: 'creating a schema set' + objectUnderTest.createSchemaSet('some dataspace','new name','cps-notification-subscriptions@2024-07-03.yang') + then: 'the operation is delegated to the module service' + 1 * mockCpsModuleService.createSchemaSet('some dataspace','new name',_) + } + + def 'Creating a schema set handles already defined exception.'() { + given: 'the module service throws an already defined exception' + mockCpsModuleService.createSchemaSet(*_) >> { throw AlreadyDefinedException.forSchemaSet('name','context',null) } + when: 'attempt to create a schema set' + objectUnderTest.createSchemaSet('some dataspace','new name','cps-notification-subscriptions@2024-07-03.yang') + then: 'the exception is ignored, and a log message is produced' + noExceptionThrown() + assertLogContains('Creating new schema set failed as schema set already exists') + } + + def 'Creating a schema set from a non-existing YANG file.'() { + when: 'attempting to create a schema set from a non-existing file' + objectUnderTest.createSchemaSet('some dataspace','some name','no such yang file') + then: 'a startup exception with correct message and details is thrown' + def thrown = thrown(ModelOnboardingException) + assert thrown.message.contains('Creating schema set failed') + assert thrown.details.contains('unable to read file') + } + + def 'Creating an anchor delegates to the service.'() { + when: 'creating an anchor' + objectUnderTest.createAnchor('some dataspace','some schema set','new name') + then: 'the operation is delegated to the anchor service' + 1 * mockCpsAnchorService.createAnchor('some dataspace','some schema set', 'new name') + } + + def 'Creating an anchor handles already defined exception.'() { + given: 'the anchor service throws an already defined exception' + mockCpsAnchorService.createAnchor(*_)>> { throw AlreadyDefinedException.forAnchor('name','context',null) } + when: 'attempting to create an anchor' + objectUnderTest.createAnchor('some dataspace','some schema set','new name') + then: 'the exception is ignored, and a log message is produced' + noExceptionThrown() + assertLogContains('Creating new anchor failed as anchor already exists') + } + + def 'Creating an anchor handles other exceptions.'() { + given: 'the anchor service throws a runtime exception' + mockCpsAnchorService.createAnchor(*_)>> { throw new RuntimeException('test message') } + when: 'attempt to create anchor' + objectUnderTest.createAnchor('some dataspace','some schema set','new name') + then: 'a startup exception with correct message and details is thrown' + def thrown = thrown(ModelOnboardingException) + assert thrown.message.contains('Creating anchor failed') + assert thrown.details.contains('test message') + } + + def 'Creating a top-level data node delegates to the service.'() { + when: 'top-level node is created' + objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node') + then: 'the correct JSON is saved using the data service' + 1 * mockCpsDataService.saveData('dataspace','anchor', '{"new node":{}}',_) + } + + def 'Creating a top-level node handles already defined exception.'() { + given: 'the data service throws an Already Defined exception' + mockCpsDataService.saveData(*_) >> { throw AlreadyDefinedException.forDataNodes([], 'some context') } + when: 'attempting to create a top-level node' + objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node') + then: 'the exception is ignored, and a log message is produced' + noExceptionThrown() + assertLogContains('failed as data node already exists') + } + + def 'Create a top-level node with any other exception.'() { + given: 'the data service throws a runtime exception' + mockCpsDataService.saveData(*_) >> { throw new RuntimeException('test message') } + when: 'attempt to create a top-level node' + objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node') + then: 'a startup exception with correct message and details is thrown' + def thrown = thrown(ModelOnboardingException) + assert thrown.message.contains('Creating data node failed') + assert thrown.details.contains('test message') + } + + def 'Delete unused schema sets delegates to the service.'() { + when: 'unused schema sets get deleted' + objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2') + then: 'a request to delete each (without cascade) is delegated to the module service' + 1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 1', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED) + 1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED) + } + + def 'Delete unused schema sets with exception.'() { + given: 'deleting the first schemaset causes an exception' + mockCpsModuleService.deleteSchemaSet(_, 'schema set 1', _) >> { throw new RuntimeException('test message')} + when: 'several unused schemas are deleted ' + objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2') + then: 'the exception message is logged' + def logs = loggingListAppender.list.toString() + assert logs.contains('Deleting schema set failed') + assert logs.contains('test message') + and: 'the second schema set is still deleted' + 1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED) + } + + def 'Update anchor schema set.'() { + when: 'a schema set for an anchor is updated' + objectUnderTest.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set') + then: 'the request is delegated to the admin service' + 1 * mockCpsAnchorService.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set') + } + + def 'Update anchor schema set with exception.'() { + given: 'the admin service throws an exception' + mockCpsAnchorService.updateAnchorSchemaSet(*_) >> { throw new RuntimeException('test message') } + when: 'a schema set for an anchor is updated' + objectUnderTest.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set') + then: 'a startup exception with correct message and details is thrown' + def thrown = thrown(ModelOnboardingException) + assert thrown.message.contains('Updating schema set failed') + assert thrown.details.contains('test message') + } + + private void assertLogContains(String message) { + def logs = loggingListAppender.list.toString() + assert logs.contains(message) + } + + class TestModelLoader extends AbstractModelLoader { + + TestModelLoader(final CpsDataspaceService cpsDataspaceService, + final CpsModuleService cpsModuleService, + final CpsAnchorService cpsAnchorService, + final CpsDataService cpsDataService) { + super(cpsDataspaceService, cpsModuleService, cpsAnchorService, cpsDataService) + } + + @Override + void onboardOrUpgradeModel() { + // No operation needed for testing + } + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy new file mode 100644 index 0000000000..0d515f90ac --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy @@ -0,0 +1,87 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 TechMahindra Ltd. + * ================================================================================ + * 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.init + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsDataspaceService +import org.onap.cps.api.CpsModuleService +import org.onap.cps.api.model.Dataspace +import org.slf4j.LoggerFactory +import org.springframework.boot.context.event.ApplicationStartedEvent +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Specification + +class CpsNotificationSubscriptionModelLoaderSpec extends Specification { + def mockCpsDataspaceService = Mock(CpsDataspaceService) + def mockCpsModuleService = Mock(CpsModuleService) + def mockCpsDataService = Mock(CpsDataService) + def mockCpsAnchorService = Mock(CpsAnchorService) + def objectUnderTest = new CpsNotificationSubscriptionModelLoader(mockCpsDataspaceService, mockCpsModuleService, mockCpsAnchorService, mockCpsDataService) + + def applicationContext = new AnnotationConfigApplicationContext() + + def expectedYangResourcesToContents + def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) + def loggingListAppender + + def CPS_DATASPACE_NAME = 'CPS-Admin' + def ANCHOR_NAME = 'cps-notification-subscriptions' + def SCHEMASET_NAME = 'cps-notification-subscriptions' + def MODEL_FILENAME = 'cps-notification-subscriptions@2024-07-03.yang' + + void setup() { + expectedYangResourcesToContents = objectUnderTest.mapYangResourcesToContent(MODEL_FILENAME) + logger.setLevel(Level.DEBUG) + loggingListAppender = new ListAppender() + logger.addAppender(loggingListAppender) + loggingListAppender.start() + applicationContext.refresh() + } + + void cleanup() { + logger.detachAndStopAllAppenders() + applicationContext.close() + loggingListAppender.stop() + } + + def 'Onboard subscription model via application started event.'() { + given: 'dataspace is already present' + mockCpsDataspaceService.getAllDataspaces() >> [new Dataspace('test')] + when: 'the application is ready' + objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent)) + then: 'the module service to create schema set is called once' + 1 * mockCpsModuleService.createSchemaSet(CPS_DATASPACE_NAME, SCHEMASET_NAME, expectedYangResourcesToContents) + and: 'the anchor service to create an anchor set is called once' + 1 * mockCpsAnchorService.createAnchor(CPS_DATASPACE_NAME, SCHEMASET_NAME, ANCHOR_NAME) + and: 'the data service to create a top level datanode is called once' + 1 * mockCpsDataService.saveData(CPS_DATASPACE_NAME, ANCHOR_NAME, '{"dataspaces":{}}', _) + } + + private void assertLogContains(String message) { + def logs = loggingListAppender.list.toString() + assert logs.contains(message) + } + +} -- cgit