Add component for deleting resources and tempates 66/130166/7
authorJozsef Csongvai <jozsef.csongvai@bell.ca>
Mon, 16 May 2022 15:15:06 +0000 (11:15 -0400)
committerkuldipr <kuldip.rai@amdocs.com>
Wed, 31 Aug 2022 21:10:23 +0000 (17:10 -0400)
Users can now add component-resource-deletion as a nodetemplate in
their CBA. This will delete resources and templates created by the
cba, using resolution key or resource-id and resource-type.

Issue-ID: CCSDK-3735
Signed-off-by: Jozsef Csongvai <jozsef.csongvai@bell.ca>
Signed-off-by: kuldipr <kuldip.rai@amdocs.com>
Change-Id: I22b7f2fe3369a3e5bac3b72a2114a81622d878dc

components/model-catalog/definition-type/starter-type/node_type/component-resource-deletion.json [new file with mode: 0644]
ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponent.kt [new file with mode: 0644]
ms/blueprintsprocessor/functions/resource-resolution/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponentTest.kt [new file with mode: 0644]

diff --git a/components/model-catalog/definition-type/starter-type/node_type/component-resource-deletion.json b/components/model-catalog/definition-type/starter-type/node_type/component-resource-deletion.json
new file mode 100644 (file)
index 0000000..7026d09
--- /dev/null
@@ -0,0 +1,67 @@
+{
+  "description": "This is Resource Deletion Component API",
+  "version": "1.0.0",
+  "attributes": {
+    "result": {
+      "description": "A map of [artifact-prefix]: { nDeletedTemplates, nDeletedResources }",
+      "required": true,
+      "type": "map"
+    },
+    "success": {
+      "required": true,
+      "type": "boolean"
+    }
+  },
+  "capabilities": {
+    "component-node": {
+      "type": "tosca.capabilities.Node"
+    }
+  },
+  "interfaces": {
+    "ResourceDeletionComponent": {
+      "operations": {
+        "process": {
+          "inputs": {
+            "resolution-key": {
+              "description": "Resolution key associated with stored assignments and templates. Required if resource-type + resource-id are empty",
+              "required": false,
+              "type": "string"
+            },
+            "resource-type": {
+              "description": "Resource-type associated with stored assignments and templates. Required if resolution-key is empty. Must be used together with resource-id",
+              "required": false,
+              "type": "string"
+            },
+            "resource-id": {
+              "description": "Resource-id associated with stored assignments and templates. Required if resolution-key is empty. Must be used together with resource-type",
+              "required": false,
+              "type": "string"
+            },
+            "artifact-prefix-names": {
+              "required": true,
+              "description": "Template , Resource Assignment Artifact Prefix names",
+              "type": "list",
+              "entry_schema": {
+                "type": "string"
+              }
+            },
+            "last-n-occurrences": {
+              "description": "Only delete last N occurrences",
+              "required": false,
+              "default": null,
+              "type": "integer"
+            },
+            "fail-on-empty": {
+              "description": "Determines if the component should fail when nothing was deleted",
+              "required": false,
+              "default": false,
+              "type": "boolean"
+            }
+          },
+          "outputs": {}
+        }
+      }
+    }
+  },
+  "derived_from": "tosca.nodes.Component"
+}
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponent.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponent.kt
new file mode 100644 (file)
index 0000000..86bcb4c
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * Copyright © 2022 Bell Canada
+ *
+ * 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.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution
+
+import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.INPUT_ARTIFACT_PREFIX_NAMES
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolutionDBService
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.TemplateResolutionService
+import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonNode
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
+import org.onap.ccsdk.cds.controllerblueprints.core.asObjectNode
+import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
+import org.springframework.beans.factory.config.ConfigurableBeanFactory
+import org.springframework.context.annotation.Scope
+import org.springframework.stereotype.Component
+
+@Component("component-resource-deletion")
+@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+open class ResourceDeletionComponent(
+    private val resourceResolutionDBService: ResourceResolutionDBService,
+    private val templateResolutionService: TemplateResolutionService
+) : AbstractComponentFunction() {
+
+    companion object {
+        const val INPUT_LAST_N_OCCURRENCES = "last-n-occurrences"
+        const val INPUT_FAIL_ON_EMPTY = "fail-on-empty"
+        const val ATTRIBUTE_RESULT = "result"
+        const val ATTRIBUTE_SUCCESS = "success"
+    }
+
+    data class DeletionResult(val nDeletedTemplates: Int, val nDeletedResources: Int)
+
+    override suspend fun processNB(executionRequest: ExecutionServiceInput) {
+        bluePrintRuntimeService.setNodeTemplateAttributeValue(
+            nodeTemplateName, ATTRIBUTE_RESULT, emptyMap<String, Any>().asJsonNode()
+        )
+        bluePrintRuntimeService.setNodeTemplateAttributeValue(
+            nodeTemplateName, ATTRIBUTE_SUCCESS, false.asJsonPrimitive()
+        )
+
+        val resolutionKey = getOptionalOperationInput(RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY)?.textValue() ?: ""
+        val resourceId = getOptionalOperationInput(RESOURCE_RESOLUTION_INPUT_RESOURCE_ID)?.textValue() ?: ""
+        val resourceType = getOptionalOperationInput(RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE)?.textValue() ?: ""
+
+        val resultMap = when {
+            resolutionKey.isNotBlank() -> runDelete(byResolutionKey(resolutionKey))
+            resourceType.isNotBlank() && resourceId.isNotBlank() ->
+                runDelete(byResourceTypeAndId(resourceType, resourceId))
+            else -> throw BluePrintProcessorException(
+                "Please use resolution-key OR resource-type + resource-id. Values must not be blank"
+            )
+        }
+        bluePrintRuntimeService.setNodeTemplateAttributeValue(
+            nodeTemplateName, ATTRIBUTE_RESULT, resultMap.asObjectNode()
+        )
+
+        getOptionalOperationInput(INPUT_FAIL_ON_EMPTY)?.booleanValue().takeIf { it == true }?.let {
+            resultMap.all { it.value.nDeletedResources == 0 && it.value.nDeletedTemplates == 0 }
+                .takeIf { it }?.let {
+                    throw BluePrintProcessorException("No templates or resources were deleted")
+                }
+        }
+
+        bluePrintRuntimeService.setNodeTemplateAttributeValue(
+            nodeTemplateName, ATTRIBUTE_SUCCESS, true.asJsonPrimitive()
+        )
+    }
+
+    private suspend fun runDelete(fn: suspend (String, String, String, Int?) -> DeletionResult):
+        Map<String, DeletionResult> {
+            val metadata = bluePrintRuntimeService.bluePrintContext().metadata!!
+            val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!!
+            val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!!
+            val artifactPrefixNamesNode = getOperationInput(INPUT_ARTIFACT_PREFIX_NAMES)
+            val artifactPrefixNames = JacksonUtils.getListFromJsonNode(artifactPrefixNamesNode, String::class.java)
+            val lastN = getOptionalOperationInput(INPUT_LAST_N_OCCURRENCES)?.let {
+                if (it.isInt) it.intValue() else null
+            }
+
+            return artifactPrefixNames.associateWith { fn(blueprintName, blueprintVersion, it, lastN) }
+        }
+
+    private fun byResolutionKey(resolutionKey: String):
+        suspend (String, String, String, Int?) -> DeletionResult = {
+            bpName, bpVersion, artifactName, lastN ->
+            val nDeleteTemplates = templateResolutionService.deleteTemplates(
+                bpName, bpVersion, artifactName, resolutionKey, lastN
+            )
+            val nDeletedResources = resourceResolutionDBService.deleteResources(
+                bpName, bpVersion, artifactName, resolutionKey, lastN
+            )
+            DeletionResult(nDeleteTemplates, nDeletedResources)
+        }
+
+    private fun byResourceTypeAndId(resourceType: String, resourceId: String):
+        suspend (String, String, String, Int?) -> DeletionResult = {
+            bpName, bpVersion, artifactName, lastN ->
+            val nDeletedTemplates = templateResolutionService.deleteTemplates(
+                bpName, bpVersion, artifactName, resourceType, resourceId, lastN
+            )
+            val nDeletedResources = resourceResolutionDBService.deleteResources(
+                bpName, bpVersion, artifactName, resourceType, resourceId, lastN
+            )
+            DeletionResult(nDeletedTemplates, nDeletedResources)
+        }
+
+    override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
+        addError(runtimeException.message ?: "Failed in ResourceDeletionComponent")
+    }
+}
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponentTest.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceDeletionComponentTest.kt
new file mode 100644 (file)
index 0000000..fb25873
--- /dev/null
@@ -0,0 +1,276 @@
+/*
+ * Copyright © 2022 Bell Canada
+ *
+ * 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.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.NullNode
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.spyk
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.ATTRIBUTE_RESULT
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.ATTRIBUTE_SUCCESS
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.INPUT_FAIL_ON_EMPTY
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceDeletionComponent.Companion.INPUT_LAST_N_OCCURRENCES
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolutionDBService
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.TemplateResolutionService
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonNode
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonType
+import org.onap.ccsdk.cds.controllerblueprints.core.data.ServiceTemplate
+import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintContext
+import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintRuntimeService
+import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
+
+class ResourceDeletionComponentTest {
+
+    private val blueprintName = "testCBA"
+    private val blueprintVersion = "1.0.0"
+    private val artifactNames = listOf("artifact-a", "artifact-b")
+    private val resolutionKey = "resolutionKey"
+    private val resourceId = "1"
+    private val resourceType = "ServiceInstance"
+    private val nodetemplateName = "resource-deletion"
+    private val executionRequest = ExecutionServiceInput()
+
+    private lateinit var resourceResolutionDBService: ResourceResolutionDBService
+    private lateinit var templateResolutionService: TemplateResolutionService
+    private lateinit var resourceDeletionComponent: ResourceDeletionComponent
+    private lateinit var bluePrintRuntimeService: BluePrintRuntimeService<*>
+
+    private val props = mutableMapOf<String, JsonNode>()
+
+    private var success = slot<JsonNode>()
+    private var result = slot<JsonNode>()
+
+    @Before
+    fun setup() {
+        bluePrintRuntimeService = spyk()
+        every { bluePrintRuntimeService.bluePrintContext() }.returns(
+            BluePrintContext(
+                ServiceTemplate().apply {
+                    this.metadata = mutableMapOf(
+                        BluePrintConstants.METADATA_TEMPLATE_VERSION to blueprintVersion,
+                        BluePrintConstants.METADATA_TEMPLATE_NAME to blueprintName
+                    )
+                }
+            )
+        )
+        every { bluePrintRuntimeService.setNodeTemplateAttributeValue(nodetemplateName, ATTRIBUTE_SUCCESS, capture(success)) }
+            .answers { }
+        every { bluePrintRuntimeService.setNodeTemplateAttributeValue(nodetemplateName, ATTRIBUTE_RESULT, capture(result)) }
+            .answers { }
+
+        resourceResolutionDBService = mockk()
+        templateResolutionService = mockk()
+        resourceDeletionComponent = ResourceDeletionComponent(resourceResolutionDBService, templateResolutionService)
+        resourceDeletionComponent.bluePrintRuntimeService = bluePrintRuntimeService
+        resourceDeletionComponent.nodeTemplateName = nodetemplateName
+        resourceDeletionComponent.executionServiceInput = executionRequest
+        resourceDeletionComponent.processId = "12"
+        resourceDeletionComponent.workflowName = "workflow"
+        resourceDeletionComponent.stepName = "step"
+        resourceDeletionComponent.interfaceName = "interfaceName"
+        resourceDeletionComponent.operationName = "operationName"
+
+        props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = NullNode.getInstance()
+        props[RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = NullNode.getInstance()
+        props[RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = NullNode.getInstance()
+        props[ResourceResolutionConstants.INPUT_ARTIFACT_PREFIX_NAMES] = artifactNames.asJsonType()
+        props[INPUT_FAIL_ON_EMPTY] = NullNode.getInstance()
+        props[INPUT_LAST_N_OCCURRENCES] = NullNode.getInstance()
+        resourceDeletionComponent.operationInputs = props
+    }
+
+    @Test
+    fun `using resolution-key`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive()
+
+        coEvery {
+            templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null)
+        }.returns(1)
+
+        coEvery {
+            resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, null)
+        }.returns(2)
+
+        runBlocking { resourceDeletionComponent.processNB(executionRequest) }
+
+        val expected = ResourceDeletionComponent.DeletionResult(1, 2).asJsonType()
+        val result: JsonNode = result.captured
+        assertEquals(expected, result[artifactNames[0]])
+        assertEquals(expected, result[artifactNames[1]])
+        assertEquals(true.asJsonPrimitive(), success.captured)
+    }
+
+    @Test
+    fun `using resource-type and resource-id`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId.asJsonPrimitive()
+        props[RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType.asJsonPrimitive()
+
+        coEvery {
+            templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resourceType, resourceId, null)
+        }.returns(2)
+
+        coEvery {
+            resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resourceType, resourceId, null)
+        }.returns(4)
+
+        runBlocking { resourceDeletionComponent.processNB(executionRequest) }
+
+        val expected = ResourceDeletionComponent.DeletionResult(2, 4).asJsonType()
+        val result: JsonNode = result.captured
+        assertEquals(expected, result[artifactNames[0]])
+        assertEquals(expected, result[artifactNames[1]])
+        assertEquals(true.asJsonPrimitive(), success.captured)
+    }
+
+    @Test(expected = BluePrintProcessorException::class)
+    fun `using resource-type missing resource-id`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType.asJsonPrimitive()
+        runBlocking { resourceDeletionComponent.processNB(executionRequest) }
+    }
+
+    @Test(expected = BluePrintProcessorException::class)
+    fun `using resource-id missing resource-type`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId.asJsonPrimitive()
+        runBlocking { resourceDeletionComponent.processNB(executionRequest) }
+    }
+
+    @Test
+    fun `attributes present when failing`() {
+        val threwException = runBlocking {
+            try {
+                resourceDeletionComponent.processNB(executionRequest)
+                false
+            } catch (e: Exception) {
+                true
+            }
+        }
+        assertTrue(threwException)
+        assertEquals(false.asJsonPrimitive(), success.captured)
+        assertEquals(emptyMap<String, Any>().asJsonNode(), result.captured)
+    }
+
+    @Test
+    fun `last-n-occurrences`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive()
+        props[INPUT_LAST_N_OCCURRENCES] = JacksonUtils.jsonNodeFromObject(3)
+
+        coEvery {
+            templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, 3)
+        }.returns(3)
+
+        coEvery {
+            resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, 3)
+        }.returns(6)
+
+        runBlocking { resourceDeletionComponent.processNB(executionRequest) }
+
+        val expected = ResourceDeletionComponent.DeletionResult(3, 6).asJsonType()
+        val result: JsonNode = result.captured
+        assertEquals(expected, result[artifactNames[0]])
+        assertEquals(expected, result[artifactNames[1]])
+        assertEquals(true.asJsonPrimitive(), success.captured)
+    }
+
+    @Test
+    fun `fail-on-empty nothing deleted`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive()
+        props[INPUT_FAIL_ON_EMPTY] = true.asJsonPrimitive()
+
+        coEvery {
+            templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null)
+        }.returns(0)
+
+        coEvery {
+            resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, null)
+        }.returns(0)
+
+        val threwException = runBlocking {
+            try {
+                resourceDeletionComponent.processNB(executionRequest)
+                false
+            } catch (e: BluePrintProcessorException) {
+                true
+            }
+        }
+
+        val expected = ResourceDeletionComponent.DeletionResult(0, 0).asJsonType()
+        val result: JsonNode = result.captured
+        assertTrue(threwException)
+        assertEquals(expected, result[artifactNames[0]])
+        assertEquals(expected, result[artifactNames[1]])
+        assertEquals(false.asJsonPrimitive(), success.captured)
+    }
+
+    @Test
+    fun `fail-on-empty something deleted`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive()
+        props[INPUT_FAIL_ON_EMPTY] = true.asJsonPrimitive()
+
+        coEvery {
+            templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null)
+        }.returns(1)
+
+        coEvery {
+            resourceResolutionDBService.deleteResources(blueprintName, blueprintVersion, any(), resolutionKey, null)
+        }.returns(1)
+
+        runBlocking { resourceDeletionComponent.processNB(executionRequest) }
+
+        val expected = ResourceDeletionComponent.DeletionResult(1, 1).asJsonType()
+        val result: JsonNode = result.captured
+        assertEquals(expected, result[artifactNames[0]])
+        assertEquals(expected, result[artifactNames[1]])
+        assertEquals(true.asJsonPrimitive(), success.captured)
+    }
+
+    @Test
+    fun `db throws exception`() {
+        props[RESOURCE_RESOLUTION_INPUT_RESOLUTION_KEY] = resolutionKey.asJsonPrimitive()
+
+        coEvery {
+            templateResolutionService.deleteTemplates(blueprintName, blueprintVersion, any(), resolutionKey, null)
+        }.throws(RuntimeException("DB failure!"))
+
+        val threwException = runBlocking {
+            try {
+                resourceDeletionComponent.processNB(executionRequest)
+                false
+            } catch (e: Exception) {
+                true
+            }
+        }
+
+        assertTrue(threwException)
+        assertEquals(false.asJsonPrimitive(), success.captured)
+        assertEquals(emptyMap<String, Any>().asJsonNode(), result.captured)
+    }
+}