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 --- .../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 +++++++ 7 files changed, 691 insertions(+) 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 (limited to 'cps-service') 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 1.2.3-korg