diff --git a/calm-hub/PERMISSIONS.md b/calm-hub/PERMISSIONS.md
index 1bbf4b40c..a88e9a62a 100644
--- a/calm-hub/PERMISSIONS.md
+++ b/calm-hub/PERMISSIONS.md
@@ -90,7 +90,12 @@ canWrite(username, namespace):
| `write` | Domain `D` | Read + write content in `D` | Flat — no hierarchy |
| `admin` | Namespace `N` | Read + write content in `N` and descendants; list/grant/revoke entitlements in `N` and descendants; create child namespaces of `N` | **OR any ancestor** |
| `admin` | Domain `D` | Read + write content in `D`; list/grant/revoke entitlements for `D` | Flat — no hierarchy |
-| `admin` | `GLOBAL` | Create/delete any namespace or domain; read + write all content; manage all entitlements (including further `GLOBAL admin` grants) | Bypasses all checks via `hasGlobalAdmin()` — only `admin` is valid; `read`/`write` grants on `GLOBAL` are rejected with 400 |
+| `admin` | `GLOBAL` | Create/delete any namespace or domain; delete any architecture, pattern, flow, standard, interface, timeline, ADR, decorator, control requirement, or control configuration; read + write all content; manage all entitlements (including further `GLOBAL admin` grants) | Bypasses all checks via `hasGlobalAdmin()` — only `admin` is valid; `read`/`write` grants on `GLOBAL` are rejected with 400 |
+
+Content-resource deletion is deliberately `GLOBAL admin`-only — a namespace- or
+domain-scoped `admin` grant does not permit it, even for content the grant
+otherwise gives full read/write access to. A control requirement refuses to
+delete (`409`) while it still has configurations; delete those first.
---
diff --git a/calm-hub/decisions/0001-versioned-artefact-storage.md b/calm-hub/decisions/0001-versioned-artefact-storage.md
index 7c30feb64..b6bfe758b 100644
--- a/calm-hub/decisions/0001-versioned-artefact-storage.md
+++ b/calm-hub/decisions/0001-versioned-artefact-storage.md
@@ -14,6 +14,13 @@ the old shape too. Both are excluded by
revisiting with the implementation experience it was waiting for. Tracked in
[#2884](https://github.com/finos/architecture-as-code/issues/2884).
+The "nothing is ever deleted" premise below described the old shape and no
+longer holds: every type this ADR covers, plus Control and Decorator, now
+has a `GLOBAL admin`-gated `DELETE` endpoint that removes a resource and all
+of its versions outright. See `PERMISSIONS.md` and
+`store/util/MongoVersionDocumentStore#deleteResource` /
+`NitriteVersionDocumentStore#deleteResource`.
+
## Context
Every Mongo store in `calm-hub` (`store/mongo/`) uses a **one document per
diff --git a/calm-hub/decisions/0003-shared-version-store-helper.md b/calm-hub/decisions/0003-shared-version-store-helper.md
index e2305a4c8..7d1eef220 100644
--- a/calm-hub/decisions/0003-shared-version-store-helper.md
+++ b/calm-hub/decisions/0003-shared-version-store-helper.md
@@ -106,8 +106,10 @@ the primitive operations every store needs against its
that fails, removing the header again. The split shape makes that
compensation necessary — the old shape wrote the resource and its first
version in one document write, so a failure left nothing behind — and
- there is no delete endpoint for any of these types, so a header stranded
- with `versionCount: 0` stays visible in listings and search permanently.
+ without it a header stranded with `versionCount: 0` stays visible in
+ listings and search until an admin notices and deletes it by hand via the
+ `deleteResource` endpoint added later; `deleteHeader` exists so that never
+ has to happen in the ordinary case.
That is why it belongs here rather than in each store. A per-store copy
is a correctness routine duplicated once per type per backend, fourteen
diff --git a/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java b/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java
index f8f41d706..1f52e4820 100644
--- a/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java
+++ b/calm-hub/src/integration-test/java/integration/MongoArchitectureIntegration.java
@@ -180,4 +180,35 @@ void end_to_end_limit_slices_the_summary_list() {
.statusCode(200)
.body("values", hasSize(2));
}
+
+ @Test
+ @Order(9)
+ void end_to_end_delete_an_architecture() {
+ given()
+ .when().delete("/api/calm/namespaces/finos/architectures/1")
+ .then()
+ .statusCode(204);
+
+ // Deleting removes the whole resource, all versions included — not just the latest.
+ given()
+ .when().get("/api/calm/namespaces/finos/architectures/1/versions/1.0.0")
+ .then()
+ .statusCode(404);
+
+ given()
+ .when().get("/api/calm/namespaces/finos/architectures")
+ .then()
+ .statusCode(200)
+ .body("values", hasSize(1))
+ .body("values[0].id", equalTo(2));
+ }
+
+ @Test
+ @Order(10)
+ void end_to_end_delete_a_missing_architecture_returns_404() {
+ given()
+ .when().delete("/api/calm/namespaces/finos/architectures/999")
+ .then()
+ .statusCode(404);
+ }
}
\ No newline at end of file
diff --git a/calm-hub/src/integration-test/java/integration/MongoControlIntegration.java b/calm-hub/src/integration-test/java/integration/MongoControlIntegration.java
index b481464cd..d1cc95021 100644
--- a/calm-hub/src/integration-test/java/integration/MongoControlIntegration.java
+++ b/calm-hub/src/integration-test/java/integration/MongoControlIntegration.java
@@ -213,11 +213,15 @@ void end_to_end_get_configurations_returns_404_for_invalid_control() {
@Test
@Order(15)
- void end_to_end_get_configuration_returns_404_for_nonexistent_config() {
+ void end_to_end_get_configuration_by_id_alone_returns_405() {
+ // There is no GET at this exact path — only .../configurations/{id}/versions[...] —
+ // so it always fell through to a 404 "no matching route". Since the DELETE endpoint
+ // now claims this exact path, JAX-RS correctly reports 405 (path matched, method
+ // didn't) instead of 404 (nothing matched).
given()
.when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/1/configurations/999")
.then()
- .statusCode(404);
+ .statusCode(405);
}
@Test
@@ -552,4 +556,98 @@ void end_to_end_create_requirement_version_stores_only_inner_json_and_updates_wr
.body("values.find { it.id == 1 }.name", equalTo("Final Access Control"))
.body("values.find { it.id == 1 }.description", equalTo("Final"));
}
+
+ // --- Delete: requirement + configuration ---
+ //
+ // Uses a freshly created control (rather than control 1, already exercised above) so this
+ // scenario is self-contained and doesn't depend on the ordering or accumulated state of the
+ // tests above.
+
+ @Test
+ @Order(50)
+ void end_to_end_delete_control_refuses_while_configurations_exist_then_succeeds() throws JsonProcessingException {
+ CreateControlRequirement requirementRequest = new CreateControlRequirement(
+ "Delete Test Control", "Control created to exercise delete", "{\"type\": \"requirement\"}");
+
+ String location = given()
+ .body(objectMapper.writeValueAsString(requirementRequest))
+ .header("Content-Type", "application/json")
+ .when().post("/api/calm/domains/" + VALID_DOMAIN + "/controls")
+ .then()
+ .statusCode(201)
+ .extract().header("Location");
+ int controlId = Integer.parseInt(location.substring(location.lastIndexOf('/') + 1));
+
+ CreateControlConfiguration configRequest = new CreateControlConfiguration("{\"setting\": \"enabled\"}");
+ String configLocation = given()
+ .body(objectMapper.writeValueAsString(configRequest))
+ .header("Content-Type", "application/json")
+ .when().post("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/configurations")
+ .then()
+ .statusCode(201)
+ .extract().header("Location");
+ int configId = Integer.parseInt(configLocation.substring(configLocation.lastIndexOf('/') + 1));
+
+ // Refuses while the configuration still exists — does not cascade.
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId)
+ .then()
+ .statusCode(409)
+ .body(containsString("configuration"));
+
+ // Requirement is untouched by the refused delete.
+ given()
+ .when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/requirement/versions/1.0.0")
+ .then()
+ .statusCode(200);
+
+ // Delete the configuration first...
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/configurations/" + configId)
+ .then()
+ .statusCode(204);
+
+ given()
+ .when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/configurations/" + configId + "/versions")
+ .then()
+ .statusCode(404);
+
+ // ...then the requirement can be deleted.
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId)
+ .then()
+ .statusCode(204);
+
+ given()
+ .when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/requirement/versions/1.0.0")
+ .then()
+ .statusCode(404);
+ }
+
+ @Test
+ @Order(51)
+ void end_to_end_delete_control_returns_404_for_missing_control() {
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/99999")
+ .then()
+ .statusCode(404);
+ }
+
+ @Test
+ @Order(52)
+ void end_to_end_delete_control_returns_404_for_invalid_domain() {
+ given()
+ .when().delete("/api/calm/domains/" + INVALID_DOMAIN + "/controls/1")
+ .then()
+ .statusCode(404);
+ }
+
+ @Test
+ @Order(53)
+ void end_to_end_delete_configuration_returns_404_for_missing_configuration() {
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/1/configurations/99999")
+ .then()
+ .statusCode(404);
+ }
}
diff --git a/calm-hub/src/integration-test/java/integration/NitriteArchitectureIntegration.java b/calm-hub/src/integration-test/java/integration/NitriteArchitectureIntegration.java
index d426d1e8a..25d6a6c26 100644
--- a/calm-hub/src/integration-test/java/integration/NitriteArchitectureIntegration.java
+++ b/calm-hub/src/integration-test/java/integration/NitriteArchitectureIntegration.java
@@ -109,4 +109,34 @@ void end_to_end_reject_malformed_json_on_versioned_put() {
.statusCode(400)
.body(containsString("could not be parsed"));
}
+
+ @Test
+ @Order(7)
+ void end_to_end_delete_an_architecture() {
+ given()
+ .when().delete("/api/calm/namespaces/finos/architectures/1")
+ .then()
+ .statusCode(204);
+
+ // Deleting removes the whole resource, all versions included — not just the latest.
+ given()
+ .when().get("/api/calm/namespaces/finos/architectures/1/versions/1.0.0")
+ .then()
+ .statusCode(404);
+
+ given()
+ .when().get("/api/calm/namespaces/finos/architectures")
+ .then()
+ .statusCode(200)
+ .body("values", empty());
+ }
+
+ @Test
+ @Order(8)
+ void end_to_end_delete_a_missing_architecture_returns_404() {
+ given()
+ .when().delete("/api/calm/namespaces/finos/architectures/999")
+ .then()
+ .statusCode(404);
+ }
}
diff --git a/calm-hub/src/integration-test/java/integration/NitriteControlIntegration.java b/calm-hub/src/integration-test/java/integration/NitriteControlIntegration.java
index a735501cd..f65801cb9 100644
--- a/calm-hub/src/integration-test/java/integration/NitriteControlIntegration.java
+++ b/calm-hub/src/integration-test/java/integration/NitriteControlIntegration.java
@@ -189,11 +189,15 @@ void end_to_end_get_configurations_returns_404_for_invalid_control() {
@Test
@Order(15)
- void end_to_end_get_configuration_returns_404_for_nonexistent_config() {
+ void end_to_end_get_configuration_by_id_alone_returns_405() {
+ // There is no GET at this exact path — only .../configurations/{id}/versions[...] —
+ // so it always fell through to a 404 "no matching route". Since the DELETE endpoint
+ // now claims this exact path, JAX-RS correctly reports 405 (path matched, method
+ // didn't) instead of 404 (nothing matched).
given()
.when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/1/configurations/999")
.then()
- .statusCode(404);
+ .statusCode(405);
}
@Test
@@ -514,4 +518,98 @@ void end_to_end_create_requirement_version_stores_only_inner_json_and_updates_wr
.body("values.find { it.id == 1 }.name", equalTo("Final Access Control"))
.body("values.find { it.id == 1 }.description", equalTo("Final"));
}
+
+ // --- Delete: requirement + configuration ---
+ //
+ // Uses a freshly created control (rather than control 1, already exercised above) so this
+ // scenario is self-contained and doesn't depend on the ordering or accumulated state of the
+ // tests above.
+
+ @Test
+ @Order(50)
+ void end_to_end_delete_control_refuses_while_configurations_exist_then_succeeds() throws JsonProcessingException {
+ CreateControlRequirement requirementRequest = new CreateControlRequirement(
+ "Delete Test Control", "Control created to exercise delete", "{\"type\": \"requirement\"}");
+
+ String location = given()
+ .body(objectMapper.writeValueAsString(requirementRequest))
+ .header("Content-Type", "application/json")
+ .when().post("/api/calm/domains/" + VALID_DOMAIN + "/controls")
+ .then()
+ .statusCode(201)
+ .extract().header("Location");
+ int controlId = Integer.parseInt(location.substring(location.lastIndexOf('/') + 1));
+
+ CreateControlConfiguration configRequest = new CreateControlConfiguration("{\"setting\": \"enabled\"}");
+ String configLocation = given()
+ .body(objectMapper.writeValueAsString(configRequest))
+ .header("Content-Type", "application/json")
+ .when().post("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/configurations")
+ .then()
+ .statusCode(201)
+ .extract().header("Location");
+ int configId = Integer.parseInt(configLocation.substring(configLocation.lastIndexOf('/') + 1));
+
+ // Refuses while the configuration still exists — does not cascade.
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId)
+ .then()
+ .statusCode(409)
+ .body(containsString("configuration"));
+
+ // Requirement is untouched by the refused delete.
+ given()
+ .when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/requirement/versions/1.0.0")
+ .then()
+ .statusCode(200);
+
+ // Delete the configuration first...
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/configurations/" + configId)
+ .then()
+ .statusCode(204);
+
+ given()
+ .when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/configurations/" + configId + "/versions")
+ .then()
+ .statusCode(404);
+
+ // ...then the requirement can be deleted.
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId)
+ .then()
+ .statusCode(204);
+
+ given()
+ .when().get("/api/calm/domains/" + VALID_DOMAIN + "/controls/" + controlId + "/requirement/versions/1.0.0")
+ .then()
+ .statusCode(404);
+ }
+
+ @Test
+ @Order(51)
+ void end_to_end_delete_control_returns_404_for_missing_control() {
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/99999")
+ .then()
+ .statusCode(404);
+ }
+
+ @Test
+ @Order(52)
+ void end_to_end_delete_control_returns_404_for_invalid_domain() {
+ given()
+ .when().delete("/api/calm/domains/" + INVALID_DOMAIN + "/controls/1")
+ .then()
+ .statusCode(404);
+ }
+
+ @Test
+ @Order(53)
+ void end_to_end_delete_configuration_returns_404_for_missing_configuration() {
+ given()
+ .when().delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/1/configurations/99999")
+ .then()
+ .statusCode(404);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/domain/exception/ControlHasConfigurationsException.java b/calm-hub/src/main/java/org/finos/calm/domain/exception/ControlHasConfigurationsException.java
new file mode 100644
index 000000000..652a1e58c
--- /dev/null
+++ b/calm-hub/src/main/java/org/finos/calm/domain/exception/ControlHasConfigurationsException.java
@@ -0,0 +1,31 @@
+package org.finos.calm.domain.exception;
+
+/**
+ * Thrown when a control requirement cannot be deleted because it still has
+ * configurations associated with it. Carries the raw control id and configuration
+ * count as fields rather than a formatted message — the resource layer (see
+ * {@code ControlResource#controlHasConfigurationsResponse}) is solely responsible
+ * for composing the user-facing message, so it isn't duplicated here.
+ */
+public class ControlHasConfigurationsException extends Exception {
+ private final int controlId;
+ private final int configurationCount;
+
+ /**
+ * @param controlId the control that could not be deleted because it still has configurations
+ * @param configurationCount how many configurations exist under the control
+ */
+ public ControlHasConfigurationsException(int controlId, int configurationCount) {
+ super("Control not empty: " + controlId);
+ this.controlId = controlId;
+ this.configurationCount = configurationCount;
+ }
+
+ public int getControlId() {
+ return controlId;
+ }
+
+ public int getConfigurationCount() {
+ return configurationCount;
+ }
+}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java
index 6336a2c8a..0a3fd7cb5 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/AdrResource.java
@@ -293,6 +293,33 @@ public Response updateAdrStatusForNamespace(
}
}
+ /**
+ * Delete an ADR and all of its revisions
+ *
+ * @param namespace the namespace the ADR is in
+ * @param adrId the ID of the ADR
+ * @return no content on success
+ */
+ @DELETE
+ @Path("{namespace}/adrs/{adrId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(
+ summary = "Delete an ADR",
+ description = "Deletes an ADR and all of its revisions from the given namespace. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteAdrForNamespace(
+ @PathParam("namespace") @Pattern(regexp= NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("adrId") int adrId
+ ) {
+ try {
+ store.deleteAdr(namespace, adrId);
+ } catch (Exception e) {
+ return handleException(e, namespace, adrId);
+ }
+ return Response.noContent().build();
+ }
+
private Response adrWithLocationResponse(AdrMeta adrMeta) throws URISyntaxException {
return Response.created(new URI("/api/calm/namespaces/" + adrMeta.getNamespace() + "/adrs/" + adrMeta.getId() + "/revisions/" + adrMeta.getRevision())).build();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/ArchitectureResource.java b/calm-hub/src/main/java/org/finos/calm/resources/ArchitectureResource.java
index 9652a54c1..2a9a41855 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/ArchitectureResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/ArchitectureResource.java
@@ -8,6 +8,7 @@
import jakarta.validation.constraints.Pattern;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
@@ -32,6 +33,7 @@
import org.finos.calm.services.ArchitectureTimelineService;
import org.finos.calm.services.CustomIdEnrichmentService;
import org.finos.calm.store.ArchitectureStore;
+import org.finos.calm.store.ResourceMappingStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,6 +58,7 @@ public class ArchitectureResource {
private final ArchitectureStore store;
private final ArchitectureTimelineService timelineService;
private final CustomIdEnrichmentService customIds;
+ private final ResourceMappingStore mappingStore;
private final Logger logger = LoggerFactory.getLogger(ArchitectureResource.class);
@@ -64,10 +67,11 @@ public class ArchitectureResource {
@Inject
public ArchitectureResource(ArchitectureStore store, ArchitectureTimelineService timelineService,
- CustomIdEnrichmentService customIds) {
+ CustomIdEnrichmentService customIds, ResourceMappingStore mappingStore) {
this.store = store;
this.timelineService = timelineService;
this.customIds = customIds;
+ this.mappingStore = mappingStore;
}
/**
@@ -301,6 +305,31 @@ public Response getArchitectureTimeline(
}
}
+ @DELETE
+ @Path("{namespace}/architectures/{architectureId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(
+ summary = "Delete an architecture",
+ description = "Deletes an architecture and all of its versions from the given namespace. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteArchitecture(
+ @PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("architectureId") int architectureId
+ ) {
+ try {
+ store.deleteArchitecture(namespace, architectureId);
+ } catch (NamespaceNotFoundException e) {
+ logger.error("Invalid namespace [{}] when deleting architecture", namespace, e);
+ return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
+ } catch (ArchitectureNotFoundException e) {
+ logger.error("Invalid architecture [{}] when deleting architecture", architectureId, e);
+ return invalidArchitectureResponse(architectureId);
+ }
+ MappingCleanup.deleteMapping(mappingStore, logger, namespace, ResourceType.ARCHITECTURE, architectureId);
+ return Response.noContent().build();
+ }
+
private Response architectureWithLocationResponse(Architecture architecture) throws URISyntaxException {
return Response.created(new URI("/api/calm/namespaces/" + architecture.getNamespace() + "/architectures/" + architecture.getId() + "/versions/" + architecture.getDotVersion())).build();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/ControlResource.java b/calm-hub/src/main/java/org/finos/calm/resources/ControlResource.java
index 3a4a3f38c..60f58304c 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/ControlResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/ControlResource.java
@@ -327,6 +327,63 @@ public Response createConfigurationForVersion(
}
}
+ @DELETE
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("{domain}/controls/{controlId}")
+ @Operation(
+ summary = "Delete a control requirement",
+ description = "Deletes a control requirement and all of its versions. Refuses if the control still has configurations. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteControlRequirement(
+ @PathParam("domain")
+ @Pattern(regexp = DOMAIN_REGEX, message = DOMAIN_MESSAGE)
+ String domain,
+ @PathParam("controlId") int controlId) {
+ try {
+ store.deleteControlRequirement(domain, controlId);
+ } catch (DomainNotFoundException e) {
+ logger.error("Invalid domain [{}] when deleting control", domain, e);
+ return invalidDomainResponse(domain);
+ } catch (ControlNotFoundException e) {
+ logger.error("Control [{}] not found in domain [{}]", controlId, domain, e);
+ return invalidControlResponse(controlId);
+ } catch (ControlHasConfigurationsException e) {
+ logger.error("Control [{}] has {} configuration(s) and cannot be deleted", controlId, e.getConfigurationCount(), e);
+ return controlHasConfigurationsResponse(e.getControlId(), e.getConfigurationCount());
+ }
+ return Response.noContent().build();
+ }
+
+ @DELETE
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("{domain}/controls/{controlId}/configurations/{configId}")
+ @Operation(
+ summary = "Delete a control configuration",
+ description = "Deletes a control configuration and all of its versions. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteControlConfiguration(
+ @PathParam("domain")
+ @Pattern(regexp = DOMAIN_REGEX, message = DOMAIN_MESSAGE)
+ String domain,
+ @PathParam("controlId") int controlId,
+ @PathParam("configId") int configId) {
+ try {
+ store.deleteControlConfiguration(domain, controlId, configId);
+ } catch (DomainNotFoundException e) {
+ logger.error("Invalid domain [{}] when deleting configuration", domain, e);
+ return invalidDomainResponse(domain);
+ } catch (ControlNotFoundException e) {
+ logger.error("Control [{}] not found in domain [{}]", controlId, domain, e);
+ return invalidControlResponse(controlId);
+ } catch (ControlConfigurationNotFoundException e) {
+ logger.error("Configuration [{}] not found for control [{}]", configId, controlId, e);
+ return invalidConfigurationResponse(configId);
+ }
+ return Response.noContent().build();
+ }
+
private Response invalidDomainResponse(String domain) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Invalid domain provided: " + STRICT_SANITIZATION_POLICY.sanitize(domain))
@@ -345,6 +402,12 @@ private Response invalidConfigurationResponse(int configId) {
.build();
}
+ private Response controlHasConfigurationsResponse(int controlId, int configurationCount) {
+ return Response.status(Response.Status.CONFLICT)
+ .entity("Control " + controlId + " has " + configurationCount + " configuration(s) and cannot be deleted")
+ .build();
+ }
+
private Response invalidVersionResponse(String version) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Version not found: " + STRICT_SANITIZATION_POLICY.sanitize(version))
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/DecoratorResource.java b/calm-hub/src/main/java/org/finos/calm/resources/DecoratorResource.java
index 0fcc7ec1b..3bebbd7be 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/DecoratorResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/DecoratorResource.java
@@ -196,4 +196,35 @@ public Response updateDecoratorForNamespace(
return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
}
}
+
+ /**
+ * Delete an existing decorator by ID in a given namespace.
+ *
+ * @param namespace the namespace containing the decorator
+ * @param id the id of the decorator to delete
+ * @return 204 No Content, or an appropriate error response
+ */
+ @DELETE
+ @Path("{namespace}/decorators/{id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(
+ summary = "Delete a decorator by ID in a given namespace",
+ description = "Deletes a decorator from the given namespace. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteDecoratorForNamespace(
+ @PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("id") @Min(value = 1, message = "ID must be a positive integer") int id
+ ) {
+ try {
+ decoratorStore.deleteDecorator(namespace, id);
+ } catch (DecoratorNotFoundException e) {
+ logger.error("Decorator [{}] not found in namespace [{}] when deleting", id, namespace, e);
+ return CalmResourceErrorResponses.decoratorNotFoundResponse(namespace, id);
+ } catch (NamespaceNotFoundException e) {
+ logger.error("Invalid namespace [{}] when deleting decorator [{}]", namespace, id, e);
+ return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
+ }
+ return Response.noContent().build();
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/FlowResource.java b/calm-hub/src/main/java/org/finos/calm/resources/FlowResource.java
index 924d59e3b..0db651ebb 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/FlowResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/FlowResource.java
@@ -23,6 +23,7 @@
import org.finos.calm.security.CalmHubScopes;
import org.finos.calm.services.CustomIdEnrichmentService;
import org.finos.calm.store.FlowStore;
+import org.finos.calm.store.ResourceMappingStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,6 +43,7 @@ public class FlowResource {
private final FlowStore store;
private final CustomIdEnrichmentService customIds;
+ private final ResourceMappingStore mappingStore;
private final Logger logger = LoggerFactory.getLogger(FlowResource.class);
@@ -49,9 +51,10 @@ public class FlowResource {
Boolean allowPutOperations;
@Inject
- public FlowResource(FlowStore store, CustomIdEnrichmentService customIds) {
+ public FlowResource(FlowStore store, CustomIdEnrichmentService customIds, ResourceMappingStore mappingStore) {
this.store = store;
this.customIds = customIds;
+ this.mappingStore = mappingStore;
}
@GET
@@ -274,6 +277,31 @@ public Response updateVersionedFlow(
}
}
+ @DELETE
+ @Path("{namespace}/flows/{flowId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(
+ summary = "Delete a flow",
+ description = "Deletes a flow and all of its versions from the given namespace. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteFlow(
+ @PathParam("namespace") @Pattern(regexp= NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("flowId") int flowId
+ ) {
+ try {
+ store.deleteFlow(namespace, flowId);
+ } catch (NamespaceNotFoundException e) {
+ logger.error("Invalid namespace [{}] when deleting flow", namespace, e);
+ return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
+ } catch (FlowNotFoundException e) {
+ logger.error("Invalid flow [{}] when deleting flow", flowId, e);
+ return invalidFlowResponse(flowId);
+ }
+ MappingCleanup.deleteMapping(mappingStore, logger, namespace, ResourceType.FLOW, flowId);
+ return Response.noContent().build();
+ }
+
private Response flowWithLocationResponse(Flow flow) throws URISyntaxException {
return Response.created(new URI("/api/calm/namespaces/" + flow.getNamespace() + "/flows/" + flow.getId() + "/versions/" + flow.getDotVersion())).build();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/InterfaceResource.java b/calm-hub/src/main/java/org/finos/calm/resources/InterfaceResource.java
index 3059fd74b..b4871d117 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/InterfaceResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/InterfaceResource.java
@@ -21,6 +21,7 @@
import org.finos.calm.security.CalmHubScopes;
import org.finos.calm.services.CustomIdEnrichmentService;
import org.finos.calm.store.InterfaceStore;
+import org.finos.calm.store.ResourceMappingStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,13 +36,15 @@ public class InterfaceResource {
private final InterfaceStore interfaceStore;
private final CustomIdEnrichmentService customIds;
+ private final ResourceMappingStore mappingStore;
private final Logger logger = LoggerFactory.getLogger(InterfaceResource.class);
@Inject
- public InterfaceResource(InterfaceStore interfaceStore, CustomIdEnrichmentService customIds) {
+ public InterfaceResource(InterfaceStore interfaceStore, CustomIdEnrichmentService customIds, ResourceMappingStore mappingStore) {
this.interfaceStore = interfaceStore;
this.customIds = customIds;
+ this.mappingStore = mappingStore;
}
@GET
@@ -152,6 +155,27 @@ public Response createInterfaceForVersion(
}
}
+ @DELETE
+ @Path("{namespace}/interfaces/{interfaceId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteInterface(
+ @PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("interfaceId") Integer interfaceId
+ ) {
+ try {
+ interfaceStore.deleteInterface(namespace, interfaceId);
+ } catch (NamespaceNotFoundException e) {
+ logger.error("Invalid namespace [{}] when deleting interface", namespace, e);
+ return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
+ } catch (InterfaceNotFoundException e) {
+ logger.error("Invalid interface [{}] when deleting interface", interfaceId, e);
+ return invalidInterfaceResponse(interfaceId);
+ }
+ MappingCleanup.deleteMapping(mappingStore, logger, namespace, ResourceType.INTERFACE, interfaceId);
+ return Response.noContent().build();
+ }
+
private Response invalidInterfaceResponse(int interfaceId) {
return Response.status(Response.Status.NOT_FOUND).entity("Invalid interface provided: " + interfaceId).build();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/MappingCleanup.java b/calm-hub/src/main/java/org/finos/calm/resources/MappingCleanup.java
new file mode 100644
index 000000000..530d30d1d
--- /dev/null
+++ b/calm-hub/src/main/java/org/finos/calm/resources/MappingCleanup.java
@@ -0,0 +1,43 @@
+package org.finos.calm.resources;
+
+import org.finos.calm.domain.ResourceType;
+import org.finos.calm.domain.exception.NamespaceNotFoundException;
+import org.finos.calm.store.ResourceMappingStore;
+import org.slf4j.Logger;
+
+/**
+ * Best-effort cleanup of a resource's custom-id mapping after its underlying resource has
+ * already been deleted, shared by the five resource types the name-based ({@code /calm}) API
+ * can map: Architecture, Flow, Interface, Pattern, Standard.
+ *
+ *
Without this, deleting a resource that was created (or is also reachable) through the
+ * name-based API leaves its {@code resource_mappings} entry behind: recreating it under the
+ * same custom ID then fails with {@code DuplicateMappingException}, and the custom-id route
+ * keeps resolving to a numeric ID that no longer exists.
+ *
+ * Most resources have no mapping at all — a custom ID only exists for resources written
+ * through the name-based API — so {@link ResourceMappingStore#deleteMappingByNumericId} is a
+ * no-op in the common case. The underlying resource is already gone by the time this runs, so
+ * a failure here is logged and swallowed rather than failing the delete response over cleanup
+ * that can't be rolled back into an undelete anyway.
+ */
+final class MappingCleanup {
+
+ private MappingCleanup() {
+ }
+
+ static void deleteMapping(ResourceMappingStore mappingStore, Logger logger,
+ String namespace, ResourceType type, int numericId) {
+ try {
+ mappingStore.deleteMappingByNumericId(namespace, type, numericId);
+ } catch (NamespaceNotFoundException e) {
+ logger.warn("Could not clean up the mapping for {} [{}] in namespace [{}] after delete "
+ + "— the namespace no longer exists", type, numericId, namespace, e);
+ } catch (RuntimeException e) {
+ // A driver/DB failure (MongoException, a Nitrite lock/store error, ...) must not
+ // surface as an unhandled 500 for a delete that has already succeeded.
+ logger.warn("Could not clean up the mapping for {} [{}] in namespace [{}] after delete",
+ type, numericId, namespace, e);
+ }
+ }
+}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/PatternResource.java b/calm-hub/src/main/java/org/finos/calm/resources/PatternResource.java
index df6bf153b..deafc0903 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/PatternResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/PatternResource.java
@@ -22,6 +22,7 @@
import org.finos.calm.security.CalmHubScopes;
import org.finos.calm.services.CustomIdEnrichmentService;
import org.finos.calm.store.PatternStore;
+import org.finos.calm.store.ResourceMappingStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,6 +41,7 @@ public class PatternResource {
private final PatternStore store;
private final CustomIdEnrichmentService customIds;
+ private final ResourceMappingStore mappingStore;
private final Logger logger = LoggerFactory.getLogger(PatternResource.class);
@@ -47,9 +49,10 @@ public class PatternResource {
Boolean allowPutOperations;
@Inject
- public PatternResource(PatternStore store, CustomIdEnrichmentService customIds) {
+ public PatternResource(PatternStore store, CustomIdEnrichmentService customIds, ResourceMappingStore mappingStore) {
this.store = store;
this.customIds = customIds;
+ this.mappingStore = mappingStore;
}
@GET
@@ -248,6 +251,31 @@ public Response updateVersionedPattern(
}
+ @DELETE
+ @Path("{namespace}/patterns/{patternId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(
+ summary = "Delete a pattern",
+ description = "Deletes a pattern and all of its versions from the given namespace. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deletePattern(
+ @PathParam("namespace") @jakarta.validation.constraints.Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("patternId") int patternId
+ ) {
+ try {
+ store.deletePattern(namespace, patternId);
+ } catch (NamespaceNotFoundException e) {
+ logger.error("Invalid namespace [{}] when deleting pattern", namespace, e);
+ return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
+ } catch (PatternNotFoundException e) {
+ logger.error("Invalid pattern [{}] when deleting pattern", patternId, e);
+ return invalidPatternResponse(patternId);
+ }
+ MappingCleanup.deleteMapping(mappingStore, logger, namespace, ResourceType.PATTERN, patternId);
+ return Response.noContent().build();
+ }
+
private Response patternWithLocationResponse(Pattern pattern) throws URISyntaxException {
return Response.created(new URI("/api/calm/namespaces/" + pattern.getNamespace() + "/patterns/" + pattern.getId() + "/versions/" + pattern.getDotVersion())).build();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java b/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java
index 07ede7013..5f79eb610 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/StandardResource.java
@@ -16,6 +16,7 @@
import org.finos.calm.domain.standards.CreateStandardRequest;
import org.finos.calm.security.CalmHubScopes;
import org.finos.calm.services.CustomIdEnrichmentService;
+import org.finos.calm.store.ResourceMappingStore;
import org.finos.calm.store.StandardStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,12 +32,14 @@ public class StandardResource {
private final StandardStore standardStore;
private final CustomIdEnrichmentService customIds;
+ private final ResourceMappingStore mappingStore;
private final Logger logger = LoggerFactory.getLogger(StandardResource.class);
- public StandardResource(StandardStore standardStore, CustomIdEnrichmentService customIds) {
+ public StandardResource(StandardStore standardStore, CustomIdEnrichmentService customIds, ResourceMappingStore mappingStore) {
this.standardStore = standardStore;
this.customIds = customIds;
+ this.mappingStore = mappingStore;
}
@GET
@@ -142,6 +145,27 @@ public Response createStandardForVersion(
}
}
+ @DELETE
+ @Path("{namespace}/standards/{standardId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteStandard(
+ @PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("standardId") Integer standardId
+ ) {
+ try {
+ standardStore.deleteStandard(namespace, standardId);
+ } catch (NamespaceNotFoundException e) {
+ logger.error("Invalid namespace [{}] when deleting standard", namespace, e);
+ return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
+ } catch (StandardNotFoundException e) {
+ logger.error("Invalid standard [{}] when deleting standard", standardId, e);
+ return invalidStandardResponse(standardId);
+ }
+ MappingCleanup.deleteMapping(mappingStore, logger, namespace, ResourceType.STANDARD, standardId);
+ return Response.noContent().build();
+ }
+
private Response invalidStandardResponse(int standardId) {
return Response.status(Response.Status.NOT_FOUND).entity("Invalid standard provided: " + standardId).build();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java b/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java
index a8b64f1ca..e79faf25f 100644
--- a/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java
+++ b/calm-hub/src/main/java/org/finos/calm/resources/TimelineResource.java
@@ -238,6 +238,30 @@ public Response updateVersionedTimeline(
}
}
+ @DELETE
+ @Path("{namespace}/timelines/{timelineId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(
+ summary = "Delete a timeline",
+ description = "Deletes a timeline and all of its versions from the given namespace. Requires global admin privilege."
+ )
+ @PermissionsAllowed(CalmHubScopes.GLOBAL_ADMIN)
+ public Response deleteTimeline(
+ @PathParam("namespace") @Pattern(regexp = NAMESPACE_REGEX, message = NAMESPACE_MESSAGE) String namespace,
+ @PathParam("timelineId") int timelineId
+ ) {
+ try {
+ store.deleteTimeline(namespace, timelineId);
+ } catch (NamespaceNotFoundException e) {
+ logger.error("Invalid namespace [{}] when deleting timeline", namespace, e);
+ return CalmResourceErrorResponses.invalidNamespaceResponse(namespace);
+ } catch (TimelineNotFoundException e) {
+ logger.error("Invalid timeline [{}] when deleting timeline", timelineId, e);
+ return invalidTimelineResponse(timelineId);
+ }
+ return Response.noContent().build();
+ }
+
private Response timelineWithLocationResponse(Timeline timeline) throws URISyntaxException {
return Response.created(new URI("/api/calm/namespaces/" + timeline.getNamespace() + "/timelines/" + timeline.getId() + "/versions/" + timeline.getDotVersion())).build();
}
diff --git a/calm-hub/src/main/java/org/finos/calm/security/AuditRequestFilter.java b/calm-hub/src/main/java/org/finos/calm/security/AuditRequestFilter.java
index d29093037..549ede538 100644
--- a/calm-hub/src/main/java/org/finos/calm/security/AuditRequestFilter.java
+++ b/calm-hub/src/main/java/org/finos/calm/security/AuditRequestFilter.java
@@ -353,6 +353,10 @@ private AuditContext resolveControlResource(String methodName, UriInfo uriInfo,
AuditEntityType.CONTROL_CONFIGURATION, domain, outcome, responseContext);
case "createConfigurationForVersion" -> new AuditContext(AuditEntityType.CONTROL_CONFIGURATION,
AuditAction.UPDATE, null, domain, uriInfo.getPathParameters().getFirst("configId"), version);
+ case "deleteControlRequirement" -> new AuditContext(AuditEntityType.CONTROL_REQUIREMENT,
+ AuditAction.DELETE, null, domain, uriInfo.getPathParameters().getFirst("controlId"), null);
+ case "deleteControlConfiguration" -> new AuditContext(AuditEntityType.CONTROL_CONFIGURATION,
+ AuditAction.DELETE, null, domain, uriInfo.getPathParameters().getFirst("configId"), null);
default -> null;
};
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java
index 3c6d6f619..7d583093b 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/AdrStore.java
@@ -32,4 +32,9 @@ public interface AdrStore {
AdrMeta getAdrRevision(AdrMeta adrMeta) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrParseException;
AdrMeta updateAdrForNamespace(AdrMeta adrMeta) throws NamespaceNotFoundException, AdrNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException, AdrRevisionExistsException;
AdrMeta updateAdrStatus(AdrMeta adrMeta, Status status) throws AdrNotFoundException, NamespaceNotFoundException, AdrRevisionNotFoundException, AdrPersistenceException, AdrParseException, AdrRevisionExistsException;
+
+ /**
+ * Deletes an ADR and all of its revisions.
+ */
+ void deleteAdr(String namespace, int adrId) throws NamespaceNotFoundException, AdrNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/ArchitectureStore.java b/calm-hub/src/main/java/org/finos/calm/store/ArchitectureStore.java
index c0d475fdd..9279a03c0 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/ArchitectureStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/ArchitectureStore.java
@@ -35,4 +35,9 @@ default List getArchitecturesForNamespace(String names
String getArchitectureForVersion(Architecture architecture) throws NamespaceNotFoundException, ArchitectureNotFoundException, ArchitectureVersionNotFoundException;
Architecture createArchitectureForVersion(Architecture architecture) throws NamespaceNotFoundException, ArchitectureNotFoundException, ArchitectureVersionExistsException;
Architecture updateArchitectureForVersion(Architecture architecture) throws NamespaceNotFoundException, ArchitectureNotFoundException;
+
+ /**
+ * Deletes an architecture and all of its versions.
+ */
+ void deleteArchitecture(String namespace, int architectureId) throws NamespaceNotFoundException, ArchitectureNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/ControlStore.java b/calm-hub/src/main/java/org/finos/calm/store/ControlStore.java
index a57619b8e..848c1edc3 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/ControlStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/ControlStore.java
@@ -7,6 +7,7 @@
import org.finos.calm.domain.exception.ControlConfigurationNotFoundException;
import org.finos.calm.domain.exception.ControlConfigurationVersionNotFoundException;
import org.finos.calm.domain.exception.ControlConfigurationVersionExistsException;
+import org.finos.calm.domain.exception.ControlHasConfigurationsException;
import org.finos.calm.domain.exception.ControlNotFoundException;
import org.finos.calm.domain.exception.ControlRequirementVersionExistsException;
import org.finos.calm.domain.exception.ControlRequirementVersionNotFoundException;
@@ -29,4 +30,15 @@ public interface ControlStore {
List getConfigurationVersions(String domain, int controlId, int configurationId) throws DomainNotFoundException, ControlNotFoundException, ControlConfigurationNotFoundException;
String getConfigurationForVersion(String domain, int controlId, int configurationId, String version) throws DomainNotFoundException, ControlNotFoundException, ControlConfigurationNotFoundException, ControlConfigurationVersionNotFoundException;
void createConfigurationForVersion(String domain, int controlId, int configurationId, String version, CreateControlConfiguration request) throws DomainNotFoundException, ControlNotFoundException, ControlConfigurationNotFoundException, ControlConfigurationVersionExistsException;
+
+ /**
+ * Deletes a control requirement and all of its versions. Refuses if the control still
+ * has configurations, rather than cascading — see {@link ControlHasConfigurationsException}.
+ */
+ void deleteControlRequirement(String domain, int controlId) throws DomainNotFoundException, ControlNotFoundException, ControlHasConfigurationsException;
+
+ /**
+ * Deletes a control configuration and all of its versions.
+ */
+ void deleteControlConfiguration(String domain, int controlId, int configurationId) throws DomainNotFoundException, ControlNotFoundException, ControlConfigurationNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/DecoratorStore.java b/calm-hub/src/main/java/org/finos/calm/store/DecoratorStore.java
index 3c74750d6..971f22225 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/DecoratorStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/DecoratorStore.java
@@ -64,4 +64,14 @@ public interface DecoratorStore {
* @throws DecoratorNotFoundException if no decorator with the given ID exists
*/
void updateDecorator(String namespace, int id, String decoratorJson) throws NamespaceNotFoundException, DecoratorNotFoundException;
+
+ /**
+ * Delete an existing decorator in the given namespace.
+ *
+ * @param namespace the namespace containing the decorator
+ * @param id the ID of the decorator to delete
+ * @throws NamespaceNotFoundException if the namespace does not exist
+ * @throws DecoratorNotFoundException if no decorator with the given ID exists
+ */
+ void deleteDecorator(String namespace, int id) throws NamespaceNotFoundException, DecoratorNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/FlowStore.java b/calm-hub/src/main/java/org/finos/calm/store/FlowStore.java
index 77d7c9be0..43211b3c3 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/FlowStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/FlowStore.java
@@ -17,4 +17,9 @@ public interface FlowStore {
String getFlowForVersion(Flow flow) throws NamespaceNotFoundException, FlowNotFoundException, FlowVersionNotFoundException;
Flow createFlowForVersion(Flow flow) throws NamespaceNotFoundException, FlowNotFoundException, FlowVersionExistsException;
Flow updateFlowForVersion(Flow flow) throws NamespaceNotFoundException, FlowNotFoundException;
+
+ /**
+ * Deletes a flow and all of its versions.
+ */
+ void deleteFlow(String namespace, int flowId) throws NamespaceNotFoundException, FlowNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/InterfaceStore.java b/calm-hub/src/main/java/org/finos/calm/store/InterfaceStore.java
index 1eede59dd..69ef23aa0 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/InterfaceStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/InterfaceStore.java
@@ -16,4 +16,9 @@ public interface InterfaceStore {
List getInterfaceVersions(String namespace, Integer interfaceId) throws NamespaceNotFoundException, InterfaceNotFoundException;
String getInterfaceForVersion(String namespace, Integer interfaceId, String version) throws NamespaceNotFoundException, InterfaceNotFoundException, InterfaceVersionNotFoundException;
CalmInterface createInterfaceForVersion(CreateInterfaceRequest interfaceRequest, String namespace, Integer interfaceId, String version) throws NamespaceNotFoundException, InterfaceNotFoundException, InterfaceVersionExistsException;
+
+ /**
+ * Deletes an interface and all of its versions.
+ */
+ void deleteInterface(String namespace, Integer interfaceId) throws NamespaceNotFoundException, InterfaceNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/PatternStore.java b/calm-hub/src/main/java/org/finos/calm/store/PatternStore.java
index 4a12bbf62..de5bf622b 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/PatternStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/PatternStore.java
@@ -37,4 +37,9 @@ default List getPatternsForNamespace(String namespace)
String getPatternForVersion(Pattern pattern) throws NamespaceNotFoundException, PatternNotFoundException, PatternVersionNotFoundException;
Pattern createPatternForVersion(Pattern pattern) throws NamespaceNotFoundException, PatternNotFoundException, PatternVersionExistsException;
Pattern updatePatternForVersion(Pattern pattern) throws NamespaceNotFoundException, PatternNotFoundException;
+
+ /**
+ * Deletes a pattern and all of its versions.
+ */
+ void deletePattern(String namespace, int patternId) throws NamespaceNotFoundException, PatternNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/ResourceMappingStore.java b/calm-hub/src/main/java/org/finos/calm/store/ResourceMappingStore.java
index 837a7ad7a..cdc194b75 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/ResourceMappingStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/ResourceMappingStore.java
@@ -16,4 +16,14 @@ public interface ResourceMappingStore {
List listMappingsByNumericIds(String namespace, ResourceType type, List ids) throws NamespaceNotFoundException;
void updateMappingNumericId(String namespace, ResourceType type, String customId, int numericId) throws MappingNotFoundException, NamespaceNotFoundException;
void deleteMapping(String namespace, ResourceType type, String customId) throws MappingNotFoundException, NamespaceNotFoundException;
+
+ /**
+ * Deletes the mapping for a resource by its numeric ID, if one exists.
+ *
+ * A custom ID exists only for resources written through the name-based ({@code /calm})
+ * API, so most numeric-ID resources have no mapping to clean up at all. This is a no-op
+ * rather than throwing when nothing matches, so a resource delete can call it
+ * unconditionally instead of first checking whether a mapping exists.
+ */
+ void deleteMappingByNumericId(String namespace, ResourceType type, int numericId) throws NamespaceNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/StandardStore.java b/calm-hub/src/main/java/org/finos/calm/store/StandardStore.java
index f79420edb..6f54f2940 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/StandardStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/StandardStore.java
@@ -16,4 +16,9 @@ public interface StandardStore {
List getStandardVersions(String namespace, Integer standardId) throws NamespaceNotFoundException, StandardNotFoundException;
String getStandardForVersion(String namespace, Integer standardId, String version) throws NamespaceNotFoundException, StandardNotFoundException, StandardVersionNotFoundException;
Standard createStandardForVersion(CreateStandardRequest standardRequest, String namespace, Integer standardId, String version) throws NamespaceNotFoundException, StandardNotFoundException, StandardVersionExistsException;
+
+ /**
+ * Deletes a standard and all of its versions.
+ */
+ void deleteStandard(String namespace, Integer standardId) throws NamespaceNotFoundException, StandardNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/TimelineStore.java b/calm-hub/src/main/java/org/finos/calm/store/TimelineStore.java
index 3d47fe23e..9798bfc50 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/TimelineStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/TimelineStore.java
@@ -17,4 +17,9 @@ public interface TimelineStore {
String getTimelineForVersion(Timeline timeline) throws NamespaceNotFoundException, TimelineNotFoundException, TimelineVersionNotFoundException;
Timeline createTimelineForVersion(Timeline timeline) throws NamespaceNotFoundException, TimelineNotFoundException, TimelineVersionExistsException;
Timeline updateTimelineForVersion(Timeline timeline) throws NamespaceNotFoundException, TimelineNotFoundException;
+
+ /**
+ * Deletes a timeline and all of its versions.
+ */
+ void deleteTimeline(String namespace, int timelineId) throws NamespaceNotFoundException, TimelineNotFoundException;
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java
index 79a322a71..a5ea4cdb9 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoAdrStore.java
@@ -301,4 +301,12 @@ private void requireAdr(AdrMeta adrMeta) throws NamespaceNotFoundException, AdrN
throw new AdrNotFoundException();
}
}
+
+ @Override
+ public void deleteAdr(String namespace, int adrId) throws NamespaceNotFoundException, AdrNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, adrId)) {
+ throw new AdrNotFoundException();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoArchitectureStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoArchitectureStore.java
index e33c16a89..fc3815d46 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoArchitectureStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoArchitectureStore.java
@@ -156,4 +156,12 @@ private void requireArchitecture(Architecture architecture) throws NamespaceNotF
throw new ArchitectureNotFoundException();
}
}
+
+ @Override
+ public void deleteArchitecture(String namespace, int architectureId) throws NamespaceNotFoundException, ArchitectureNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, architectureId)) {
+ throw new ArchitectureNotFoundException();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoControlStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoControlStore.java
index 2b145732d..2b309fe98 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoControlStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoControlStore.java
@@ -12,6 +12,7 @@
import org.finos.calm.domain.exception.ControlConfigurationNotFoundException;
import org.finos.calm.domain.exception.ControlConfigurationVersionExistsException;
import org.finos.calm.domain.exception.ControlConfigurationVersionNotFoundException;
+import org.finos.calm.domain.exception.ControlHasConfigurationsException;
import org.finos.calm.domain.exception.ControlNotFoundException;
import org.finos.calm.domain.exception.ControlRequirementVersionExistsException;
import org.finos.calm.domain.exception.ControlRequirementVersionNotFoundException;
@@ -194,6 +195,42 @@ public void createConfigurationForVersion(String domain, int controlId, int conf
}
}
+ /**
+ * Refuses to delete a control requirement that still has configurations, rather than
+ * cascading — see {@link ControlHasConfigurationsException}.
+ *
+ * Known, accepted race: the configuration count and the delete are two separate
+ * calls, not one atomic operation, so a configuration created via
+ * {@link #createConfigurationForVersion} in the gap between them survives under a
+ * requirement that has just been deleted. Not fixed here, consistent with the rest of this
+ * store layer using no transactions or locks anywhere else: a Nitrite-only lock would not
+ * extend the guarantee to Mongo (this backend), and the failure mode — one configuration
+ * document outliving the requirement it belonged to — is a data-hygiene issue, discoverable
+ * and cleanable, not a correctness or security one.
+ */
+ @Override
+ public void deleteControlRequirement(String domain, int controlId) throws DomainNotFoundException, ControlNotFoundException, ControlHasConfigurationsException {
+ requireControl(domain, controlId);
+
+ int configurationCount = configurationStore.countHeaders(configurationNamespace(domain, controlId));
+ if (configurationCount > 0) {
+ throw new ControlHasConfigurationsException(controlId, configurationCount);
+ }
+
+ if (!requirementStore.deleteResource(domain, controlId)) {
+ throw new ControlNotFoundException();
+ }
+ }
+
+ @Override
+ public void deleteControlConfiguration(String domain, int controlId, int configurationId) throws DomainNotFoundException, ControlNotFoundException, ControlConfigurationNotFoundException {
+ requireConfiguration(domain, controlId, configurationId);
+
+ if (!configurationStore.deleteResource(configurationNamespace(domain, controlId), configurationId)) {
+ throw new ControlConfigurationNotFoundException();
+ }
+ }
+
private void requireControl(String domain, int controlId) throws DomainNotFoundException, ControlNotFoundException {
validateDomain(domain);
if (!requirementStore.headerExists(domain, controlId)) {
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoDecoratorStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoDecoratorStore.java
index 903709145..af072620d 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoDecoratorStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoDecoratorStore.java
@@ -145,6 +145,22 @@ public void updateDecorator(String namespace, int id, String decoratorJson) thro
LOG.debug("Updated decorator with ID {} in namespace '{}'", id, namespace);
}
+ @Override
+ public void deleteDecorator(String namespace, int id) throws NamespaceNotFoundException, DecoratorNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+
+ long modified = decoratorCollection.updateOne(
+ Filters.eq("namespace", namespace),
+ Updates.pull("decorators", Filters.eq(DECORATOR_ID_FIELD, id))
+ ).getModifiedCount();
+
+ if (modified == 0) {
+ throw new DecoratorNotFoundException();
+ }
+
+ LOG.debug("Deleted decorator with ID {} in namespace '{}'", id, namespace);
+ }
+
/**
* Fetches the namespace document from MongoDB
*/
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoFlowStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoFlowStore.java
index 6cd01d34c..d67fe4d5f 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoFlowStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoFlowStore.java
@@ -143,4 +143,12 @@ private void requireFlow(Flow flow) throws NamespaceNotFoundException, FlowNotFo
throw new FlowNotFoundException();
}
}
+
+ @Override
+ public void deleteFlow(String namespace, int flowId) throws NamespaceNotFoundException, FlowNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, flowId)) {
+ throw new FlowNotFoundException();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoInterfaceStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoInterfaceStore.java
index c2f0f1350..e75c95cea 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoInterfaceStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoInterfaceStore.java
@@ -131,4 +131,12 @@ private void requireInterface(String namespace, Integer interfaceId) throws Name
throw new InterfaceNotFoundException();
}
}
+
+ @Override
+ public void deleteInterface(String namespace, Integer interfaceId) throws NamespaceNotFoundException, InterfaceNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, interfaceId)) {
+ throw new InterfaceNotFoundException();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoPatternStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoPatternStore.java
index 73c810612..35dfed3d7 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoPatternStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoPatternStore.java
@@ -152,4 +152,12 @@ private void requirePattern(Pattern pattern) throws NamespaceNotFoundException,
throw new PatternNotFoundException();
}
}
+
+ @Override
+ public void deletePattern(String namespace, int patternId) throws NamespaceNotFoundException, PatternNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, patternId)) {
+ throw new PatternNotFoundException();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoResourceMappingStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoResourceMappingStore.java
index 0907feb3b..74ef41dd6 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoResourceMappingStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoResourceMappingStore.java
@@ -198,4 +198,17 @@ public void deleteMapping(String namespace, ResourceType type, String customId)
throw new MappingNotFoundException();
}
}
+
+ @Override
+ public void deleteMappingByNumericId(String namespace, ResourceType type, int numericId) throws NamespaceNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+
+ Bson filter = Filters.and(
+ Filters.eq("namespace", namespace),
+ Filters.eq("resourceType", type.name()),
+ Filters.eq("numericId", numericId)
+ );
+
+ mappingCollection.deleteOne(filter);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoStandardStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoStandardStore.java
index b17793a56..63c221f39 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoStandardStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoStandardStore.java
@@ -127,4 +127,12 @@ private void requireStandard(String namespace, Integer standardId) throws Namesp
throw new StandardNotFoundException();
}
}
+
+ @Override
+ public void deleteStandard(String namespace, Integer standardId) throws NamespaceNotFoundException, StandardNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, standardId)) {
+ throw new StandardNotFoundException();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoTimelineStore.java b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoTimelineStore.java
index 11fb7b289..71b22b128 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoTimelineStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/mongo/MongoTimelineStore.java
@@ -151,4 +151,12 @@ private void requireTimeline(Timeline timeline) throws NamespaceNotFoundExceptio
throw new TimelineNotFoundException();
}
}
+
+ @Override
+ public void deleteTimeline(String namespace, int timelineId) throws NamespaceNotFoundException, TimelineNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, timelineId)) {
+ throw new TimelineNotFoundException();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteAdrStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteAdrStore.java
index afb571d95..7e0aebc3c 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteAdrStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteAdrStore.java
@@ -285,4 +285,13 @@ private void requireAdr(AdrMeta adrMeta) throws NamespaceNotFoundException, AdrN
throw new AdrNotFoundException();
}
}
+
+ @Override
+ public void deleteAdr(String namespace, int adrId) throws NamespaceNotFoundException, AdrNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, adrId)) {
+ throw new AdrNotFoundException();
+ }
+ LOG.info("Deleted ADR with ID {} from namespace '{}'", adrId, namespace);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteArchitectureStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteArchitectureStore.java
index b2cf1a9d2..ab7ae4977 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteArchitectureStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteArchitectureStore.java
@@ -196,4 +196,13 @@ private void requireArchitecture(Architecture architecture) throws NamespaceNotF
namespaceStore.requireNamespace(architecture.getNamespace());
requireArchitectureExists(architecture);
}
+
+ @Override
+ public void deleteArchitecture(String namespace, int architectureId) throws NamespaceNotFoundException, ArchitectureNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, architectureId)) {
+ throw new ArchitectureNotFoundException();
+ }
+ LOG.info("Deleted architecture with ID {} from namespace '{}'", architectureId, namespace);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteControlStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteControlStore.java
index bcbcc0e75..bbaebdd34 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteControlStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteControlStore.java
@@ -12,6 +12,7 @@
import org.finos.calm.domain.exception.ControlConfigurationNotFoundException;
import org.finos.calm.domain.exception.ControlConfigurationVersionExistsException;
import org.finos.calm.domain.exception.ControlConfigurationVersionNotFoundException;
+import org.finos.calm.domain.exception.ControlHasConfigurationsException;
import org.finos.calm.domain.exception.ControlNotFoundException;
import org.finos.calm.domain.exception.ControlRequirementVersionExistsException;
import org.finos.calm.domain.exception.ControlRequirementVersionNotFoundException;
@@ -191,6 +192,41 @@ public void createConfigurationForVersion(String domain, int controlId, int conf
}
}
+ /**
+ * Refuses to delete a control requirement that still has configurations, rather than
+ * cascading — see {@link ControlHasConfigurationsException}.
+ *
+ * Known, accepted race: same non-atomic count-then-delete as
+ * {@code MongoControlStore#deleteControlRequirement} — see that method's javadoc.
+ * {@code configurationStore} and {@code requirementStore} are two separate
+ * {@link NitriteVersionDocumentStore} instances, each with its own internal lock, so
+ * neither one's locking closes the gap between the count and the delete below.
+ */
+ @Override
+ public void deleteControlRequirement(String domain, int controlId) throws DomainNotFoundException, ControlNotFoundException, ControlHasConfigurationsException {
+ requireControl(domain, controlId);
+
+ int configurationCount = configurationStore.countHeaders(configurationNamespace(domain, controlId));
+ if (configurationCount > 0) {
+ throw new ControlHasConfigurationsException(controlId, configurationCount);
+ }
+
+ if (!requirementStore.deleteResource(domain, controlId)) {
+ throw new ControlNotFoundException();
+ }
+ LOG.info("Deleted control requirement {} from domain '{}'", controlId, domain);
+ }
+
+ @Override
+ public void deleteControlConfiguration(String domain, int controlId, int configurationId) throws DomainNotFoundException, ControlNotFoundException, ControlConfigurationNotFoundException {
+ requireConfiguration(domain, controlId, configurationId);
+
+ if (!configurationStore.deleteResource(configurationNamespace(domain, controlId), configurationId)) {
+ throw new ControlConfigurationNotFoundException();
+ }
+ LOG.info("Deleted configuration {} from control {} in domain '{}'", configurationId, controlId, domain);
+ }
+
private void requireControl(String domain, int controlId) throws DomainNotFoundException, ControlNotFoundException {
validateDomain(domain);
if (!requirementStore.headerExists(domain, controlId)) {
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteDecoratorStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteDecoratorStore.java
index 0fa9e7884..89df3501c 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteDecoratorStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteDecoratorStore.java
@@ -182,6 +182,38 @@ public void updateDecorator(String namespace, int id, String decoratorJson) thro
LOG.debug("Updated decorator with ID {} in namespace '{}'", id, namespace);
}
+ @Override
+ public void deleteDecorator(String namespace, int id) throws NamespaceNotFoundException, DecoratorNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+
+ lock.lock();
+ try {
+ Document namespaceDoc = fetchNamespaceDocument(namespace);
+ if (namespaceDoc == null) {
+ throw new DecoratorNotFoundException();
+ }
+
+ TypeSafeNitriteDocument typeSafeDoc = new TypeSafeNitriteDocument<>(namespaceDoc, Document.class);
+ List decorators = typeSafeDoc.getList(DECORATORS_FIELD);
+ if (decorators == null) {
+ throw new DecoratorNotFoundException();
+ }
+
+ List mutableDecorators = new ArrayList<>(decorators);
+ boolean removed = mutableDecorators.removeIf(
+ decoratorEntry -> Integer.valueOf(id).equals(decoratorEntry.get(DECORATOR_ID_FIELD, Integer.class)));
+ if (!removed) {
+ throw new DecoratorNotFoundException();
+ }
+
+ namespaceDoc.put(DECORATORS_FIELD, mutableDecorators);
+ decoratorCollection.update(namespaceDoc);
+ LOG.debug("Deleted decorator with ID {} in namespace '{}'", id, namespace);
+ } finally {
+ lock.unlock();
+ }
+ }
+
/**
* Fetches the namespace document from NitriteDB
*/
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteFlowStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteFlowStore.java
index 2e4dec496..ebfe0786b 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteFlowStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteFlowStore.java
@@ -168,4 +168,13 @@ private void requireFlow(Flow flow) throws NamespaceNotFoundException, FlowNotFo
namespaceStore.requireNamespace(flow.getNamespace());
requireFlowExists(flow);
}
+
+ @Override
+ public void deleteFlow(String namespace, int flowId) throws NamespaceNotFoundException, FlowNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, flowId)) {
+ throw new FlowNotFoundException();
+ }
+ LOG.info("Deleted flow with ID {} from namespace '{}'", flowId, namespace);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteInterfaceStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteInterfaceStore.java
index ee3f1c0ad..b27b43ec4 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteInterfaceStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteInterfaceStore.java
@@ -157,4 +157,13 @@ private void requireInterface(String namespace, Integer interfaceId) throws Name
namespaceStore.requireNamespace(namespace);
requireInterfaceExists(namespace, interfaceId);
}
+
+ @Override
+ public void deleteInterface(String namespace, Integer interfaceId) throws NamespaceNotFoundException, InterfaceNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, interfaceId)) {
+ throw new InterfaceNotFoundException();
+ }
+ LOG.info("Deleted interface with ID {} from namespace '{}'", interfaceId, namespace);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitritePatternStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitritePatternStore.java
index d674fee70..58c1d644d 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitritePatternStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitritePatternStore.java
@@ -186,4 +186,13 @@ private void requirePattern(Pattern pattern) throws NamespaceNotFoundException,
namespaceStore.requireNamespace(pattern.getNamespace());
requirePatternExists(pattern);
}
+
+ @Override
+ public void deletePattern(String namespace, int patternId) throws NamespaceNotFoundException, PatternNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, patternId)) {
+ throw new PatternNotFoundException();
+ }
+ LOG.info("Deleted pattern with ID {} from namespace '{}'", patternId, namespace);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteResourceMappingStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteResourceMappingStore.java
index cd704b7e1..c9716fba7 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteResourceMappingStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteResourceMappingStore.java
@@ -215,4 +215,22 @@ public void deleteMapping(String namespace, ResourceType type, String customId)
lock.unlock();
}
}
+
+ @Override
+ public void deleteMappingByNumericId(String namespace, ResourceType type, int numericId) throws NamespaceNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+
+ lock.lock();
+ try {
+ Filter filter = Filter.and(where(NAMESPACE_FIELD).eq(namespace),
+ where(RESOURCE_TYPE_FIELD).eq(type.name()),
+ where(NUMERIC_ID_FIELD).eq(numericId));
+ Document existing = mappingCollection.find(filter).firstOrNull();
+ if (existing != null) {
+ mappingCollection.remove(existing);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteStandardStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteStandardStore.java
index 84d6420bf..37e3cb3cc 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteStandardStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteStandardStore.java
@@ -153,4 +153,13 @@ private void requireStandard(String namespace, Integer standardId) throws Namesp
namespaceStore.requireNamespace(namespace);
requireStandardExists(namespace, standardId);
}
+
+ @Override
+ public void deleteStandard(String namespace, Integer standardId) throws NamespaceNotFoundException, StandardNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, standardId)) {
+ throw new StandardNotFoundException();
+ }
+ LOG.info("Deleted standard with ID {} from namespace '{}'", standardId, namespace);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteTimelineStore.java b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteTimelineStore.java
index 352b9f634..0782123d0 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteTimelineStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/nitrite/NitriteTimelineStore.java
@@ -181,4 +181,13 @@ private void requireTimeline(Timeline timeline) throws NamespaceNotFoundExceptio
namespaceStore.requireNamespace(timeline.getNamespace());
requireTimelineExists(timeline);
}
+
+ @Override
+ public void deleteTimeline(String namespace, int timelineId) throws NamespaceNotFoundException, TimelineNotFoundException {
+ namespaceStore.requireNamespace(namespace);
+ if (!documentStore.deleteResource(namespace, timelineId)) {
+ throw new TimelineNotFoundException();
+ }
+ LOG.info("Deleted timeline with ID {} from namespace '{}'", timelineId, namespace);
+ }
}
diff --git a/calm-hub/src/main/java/org/finos/calm/store/util/MongoVersionDocumentStore.java b/calm-hub/src/main/java/org/finos/calm/store/util/MongoVersionDocumentStore.java
index c042d1104..91ea2fa6f 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/util/MongoVersionDocumentStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/util/MongoVersionDocumentStore.java
@@ -10,6 +10,7 @@
import com.mongodb.client.model.Sorts;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.Updates;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.bson.Document;
import org.bson.conversions.Bson;
@@ -156,8 +157,9 @@ public void createHeader(String namespace, int resourceId, String name, String d
*
* The old shape pushed the resource and its first version in one document write, so
* a failure left nothing behind. Splitting them means a failed version write can strand
- * a header that no API can remove — there is no delete endpoint for these types — and it
- * would show up in listings and search with {@code versionCount: 0} forever.
+ * a header that no API can remove — this is a narrower operation than
+ * {@link #deleteResource}, the general-purpose delete — and it would show up in listings
+ * and search with {@code versionCount: 0} forever.
*
* Not a general-purpose delete: nothing else calls this, and it deliberately does not
* touch the version collection, because the only caller has just failed to write the
@@ -175,6 +177,31 @@ public void deleteHeader(String namespace, int resourceId) {
}
}
+ /**
+ * Deletes a resource entirely: its header and every version document. This is the
+ * general-purpose delete backing a DELETE endpoint — unlike {@link #deleteHeader}, a
+ * failure here is translated and thrown rather than swallowed, and the version
+ * collection is cleared too.
+ *
+ *
Versions are removed before the header, not after: if the version delete fails,
+ * the header is still there as evidence the resource exists, rather than leaving
+ * unreachable version documents behind under a header that's already gone.
+ *
+ * @return {@code true} if a header was actually deleted, {@code false} if none existed
+ * at this (namespace, resourceId) — lets the caller distinguish "already gone" from
+ * "removed".
+ */
+ public boolean deleteResource(String namespace, int resourceId) {
+ try {
+ versionCollection.deleteMany(headerFilter(namespace, resourceId));
+ DeleteResult result = headerCollection.deleteOne(headerFilter(namespace, resourceId));
+ return result.getDeletedCount() > 0;
+ } catch (MongoException e) {
+ LOG.error("Failed to delete resource [namespace={}, {}={}]", namespace, idField, resourceId, e);
+ throw StorageWriteException.writeFailed(e);
+ }
+ }
+
/**
* Writes the first version of a newly created resource, removing the header again if
* that fails, so a half-created resource never survives the request.
diff --git a/calm-hub/src/main/java/org/finos/calm/store/util/NitriteVersionDocumentStore.java b/calm-hub/src/main/java/org/finos/calm/store/util/NitriteVersionDocumentStore.java
index 44234f116..3f7d8a89f 100644
--- a/calm-hub/src/main/java/org/finos/calm/store/util/NitriteVersionDocumentStore.java
+++ b/calm-hub/src/main/java/org/finos/calm/store/util/NitriteVersionDocumentStore.java
@@ -156,6 +156,38 @@ public void deleteHeader(String namespace, int resourceId) {
}
}
+ /**
+ * Deletes a resource entirely: its header and every version document, under a single
+ * write lock so no concurrent read observes a header with no versions or vice versa.
+ * See {@link MongoVersionDocumentStore#deleteResource} — this is the general-purpose
+ * delete backing a DELETE endpoint, unlike {@link #deleteHeader}.
+ *
+ * @return {@code true} if a header was actually removed, {@code false} if none existed.
+ */
+ public boolean deleteResource(String namespace, int resourceId) {
+ lock.writeLock().lock();
+ try {
+ Filter filter = headerFilter(namespace, resourceId);
+ Document header = headerCollection.find(filter).firstOrNull();
+ if (header == null) {
+ return false;
+ }
+ // Materialize matches before removing — mutating the collection while its
+ // cursor is still being iterated is unsafe.
+ List versions = new ArrayList<>();
+ for (Document versionDocument : versionCollection.find(filter)) {
+ versions.add(versionDocument);
+ }
+ for (Document versionDocument : versions) {
+ versionCollection.remove(versionDocument);
+ }
+ headerCollection.remove(header);
+ return true;
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
/**
* Writes the first version of a newly created resource, removing the header again if
* that fails. See {@link MongoVersionDocumentStore#createFirstVersion} — the reasoning
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java
index f01a33301..ccd399b77 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestAdrResourceShould.java
@@ -393,4 +393,27 @@ private void verifyExpectedGetAdr(String namespace) throws NamespaceNotFoundExce
verify(mockAdrStore, times(1)).getAdr(expectedAdrToRetrieveMeta);
}
+ static Stream provideParametersForDeleteAdrTests() {
+ return Stream.of(
+ Arguments.of("invalid", new NamespaceNotFoundException(), 404),
+ Arguments.of("valid", new AdrNotFoundException(), 404),
+ Arguments.of("valid", null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteAdrTests")
+ void respond_correctly_to_delete_adr(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws NamespaceNotFoundException, AdrNotFoundException {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockAdrStore).deleteAdr(namespace, 12);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/" + namespace + "/adrs/12")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockAdrStore, times(1)).deleteAdr(namespace, 12);
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java
index 78a3ad1a1..837302150 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestArchitectureResourceShould.java
@@ -7,6 +7,7 @@
import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
import org.finos.calm.domain.Architecture;
+import org.finos.calm.domain.ResourceType;
import org.finos.calm.domain.architecture.ArchitectureRequest;
import org.finos.calm.domain.namespaces.NamespaceResourceSummary;
import org.finos.calm.domain.exception.ArchitectureNotFoundException;
@@ -485,6 +486,64 @@ void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_get_archite
.body(containsString(NAMESPACE_MESSAGE));
}
+ @Test
+ void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_delete_architecture() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/fin_os/architectures/12")
+ .then()
+ .statusCode(400)
+ .body(containsString(NAMESPACE_MESSAGE));
+ }
+
+ static Stream provideParametersForDeleteArchitectureTests() {
+ return Stream.of(
+ Arguments.of("invalid", new NamespaceNotFoundException(), 404),
+ Arguments.of("valid", new ArchitectureNotFoundException(), 404),
+ Arguments.of("valid", null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteArchitectureTests")
+ void respond_correctly_to_delete_architecture(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws ArchitectureNotFoundException, NamespaceNotFoundException {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockArchitectureStore).deleteArchitecture(namespace, 12);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/" + namespace + "/architectures/12")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockArchitectureStore, times(1)).deleteArchitecture(namespace, 12);
+ }
+
+ @Test
+ void delete_architecture_also_cleans_up_its_resource_mapping() throws Exception {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/architectures/12")
+ .then()
+ .statusCode(204);
+
+ verify(mockResourceMappingStore, times(1)).deleteMappingByNumericId("valid", ResourceType.ARCHITECTURE, 12);
+ }
+
+ @Test
+ void not_clean_up_the_resource_mapping_when_deleting_a_missing_architecture() throws Exception {
+ doThrow(new ArchitectureNotFoundException()).when(mockArchitectureStore).deleteArchitecture("valid", 12);
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/architectures/12")
+ .then()
+ .statusCode(404);
+
+ verifyNoInteractions(mockResourceMappingStore);
+ }
+
@Test
void return_an_implied_timeline_projection_when_architecture_has_versions() throws NamespaceNotFoundException, ArchitectureNotFoundException {
when(mockTimelineStore.getTimelinesForNamespace(any())).thenReturn(List.of());
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java
index c440a8a3b..2e8e39934 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestControlResourceShould.java
@@ -620,4 +620,74 @@ void respond_correctly_to_create_configuration_version(Throwable exceptionToThro
.statusCode(expectedStatusCode);
}
}
+
+ @Test
+ void return_a_400_when_an_invalid_format_of_domain_is_provided_on_delete_control_requirement() {
+ given()
+ .when()
+ .delete("/api/calm/domains/invalid_domain/controls/1")
+ .then()
+ .statusCode(400)
+ .body(containsString(DOMAIN_MESSAGE));
+ }
+
+ static Stream provideParametersForDeleteControlRequirementTests() {
+ return Stream.of(
+ Arguments.of(new DomainNotFoundException(INVALID_DOMAIN), 404),
+ Arguments.of(new ControlNotFoundException(), 404),
+ Arguments.of(new ControlHasConfigurationsException(1, 2), 409),
+ Arguments.of(null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteControlRequirementTests")
+ void respond_correctly_to_delete_control_requirement(Throwable exceptionToThrow, int expectedStatusCode) throws Exception {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockControlStore).deleteControlRequirement(VALID_DOMAIN, 1);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/1")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockControlStore, times(1)).deleteControlRequirement(VALID_DOMAIN, 1);
+ }
+
+ @Test
+ void return_a_400_when_an_invalid_format_of_domain_is_provided_on_delete_control_configuration() {
+ given()
+ .when()
+ .delete("/api/calm/domains/invalid_domain/controls/1/configurations/10")
+ .then()
+ .statusCode(400)
+ .body(containsString(DOMAIN_MESSAGE));
+ }
+
+ static Stream provideParametersForDeleteControlConfigurationTests() {
+ return Stream.of(
+ Arguments.of(new DomainNotFoundException(INVALID_DOMAIN), 404),
+ Arguments.of(new ControlNotFoundException(), 404),
+ Arguments.of(new ControlConfigurationNotFoundException(), 404),
+ Arguments.of(null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteControlConfigurationTests")
+ void respond_correctly_to_delete_control_configuration(Throwable exceptionToThrow, int expectedStatusCode) throws Exception {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockControlStore).deleteControlConfiguration(VALID_DOMAIN, 1, 10);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/domains/" + VALID_DOMAIN + "/controls/1/configurations/10")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockControlStore, times(1)).deleteControlConfiguration(VALID_DOMAIN, 1, 10);
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java
index aaf617c2e..90456200e 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestDecoratorResourceShould.java
@@ -676,4 +676,62 @@ void return_400_when_put_id_is_negative() {
.statusCode(400)
.body(containsString("ID must be a positive integer"));
}
+
+ // ---- DELETE /api/calm/namespaces/{namespace}/decorators/{id} ----
+
+ @Test
+ void return_204_when_decorator_deleted_successfully() throws Exception {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/finos/decorators/1")
+ .then()
+ .statusCode(204);
+
+ verify(decoratorStore, times(1)).deleteDecorator("finos", 1);
+ }
+
+ @Test
+ void return_404_when_decorator_not_found_for_delete() throws Exception {
+ doThrow(new DecoratorNotFoundException())
+ .when(decoratorStore).deleteDecorator(anyString(), anyInt());
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/finos/decorators/999")
+ .then()
+ .statusCode(404)
+ .body(containsString("Decorator with ID 999 does not exist in namespace: finos"));
+ }
+
+ @Test
+ void return_404_when_namespace_does_not_exist_for_delete_decorator() throws Exception {
+ doThrow(new NamespaceNotFoundException())
+ .when(decoratorStore).deleteDecorator(anyString(), anyInt());
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/invalid-namespace/decorators/1")
+ .then()
+ .statusCode(404);
+ }
+
+ @Test
+ void return_400_when_namespace_has_invalid_characters_for_delete_decorator() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/invalid@namespace/decorators/1")
+ .then()
+ .statusCode(400)
+ .body(containsString("namespace must match pattern"));
+ }
+
+ @Test
+ void return_400_when_delete_id_is_zero() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/test-namespace/decorators/0")
+ .then()
+ .statusCode(400)
+ .body(containsString("ID must be a positive integer"));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java
index 2dc4a522d..45e11d488 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestFlowResourceShould.java
@@ -5,6 +5,7 @@
import io.quarkus.test.security.TestSecurity;
import org.bson.json.JsonParseException;
import org.finos.calm.domain.Flow;
+import org.finos.calm.domain.ResourceType;
import org.finos.calm.domain.exception.FlowNotFoundException;
import org.finos.calm.domain.exception.FlowVersionExistsException;
import org.finos.calm.domain.exception.FlowVersionNotFoundException;
@@ -23,6 +24,7 @@
import java.util.stream.Stream;
import static io.restassured.RestAssured.given;
+import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
import static org.finos.calm.resources.ResourceValidationConstants.VERSION_MESSAGE;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
@@ -371,4 +373,62 @@ void return_forbidden_for_put_operations_on_flows_default_and_when_configured()
.then()
.statusCode(403);
}
+
+ @Test
+ void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_delete_flow() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/fin_os/flows/12")
+ .then()
+ .statusCode(400)
+ .body(containsString(NAMESPACE_MESSAGE));
+ }
+
+ static Stream provideParametersForDeleteFlowTests() {
+ return Stream.of(
+ Arguments.of("invalid", new NamespaceNotFoundException(), 404),
+ Arguments.of("valid", new FlowNotFoundException(), 404),
+ Arguments.of("valid", null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteFlowTests")
+ void respond_correctly_to_delete_flow(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws FlowNotFoundException, NamespaceNotFoundException {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockFlowStore).deleteFlow(namespace, 12);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/" + namespace + "/flows/12")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockFlowStore, times(1)).deleteFlow(namespace, 12);
+ }
+
+ @Test
+ void delete_flow_also_cleans_up_its_resource_mapping() throws Exception {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/flows/12")
+ .then()
+ .statusCode(204);
+
+ verify(mockResourceMappingStore, times(1)).deleteMappingByNumericId("valid", ResourceType.FLOW, 12);
+ }
+
+ @Test
+ void not_clean_up_the_resource_mapping_when_deleting_a_missing_flow() throws Exception {
+ doThrow(new FlowNotFoundException()).when(mockFlowStore).deleteFlow("valid", 12);
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/flows/12")
+ .then()
+ .statusCode(404);
+
+ verifyNoInteractions(mockResourceMappingStore);
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java
index f92c7cc55..34e11e8ec 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestInterfaceResourceShould.java
@@ -393,4 +393,62 @@ void respond_correctly_to_create_interfaces(String namespace, Throwable exceptio
verify(mockInterfaceStore, times(1)).createInterfaceForVersion(createInterfaceRequest, namespace, 5, "1.0.1");
}
+
+ @Test
+ void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_delete_interface() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/fin_os/interfaces/12")
+ .then()
+ .statusCode(400)
+ .body(containsString(NAMESPACE_MESSAGE));
+ }
+
+ static Stream provideParametersForDeleteInterfaceTests() {
+ return Stream.of(
+ Arguments.of("invalid", new NamespaceNotFoundException(), 404),
+ Arguments.of("valid", new InterfaceNotFoundException(), 404),
+ Arguments.of("valid", null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteInterfaceTests")
+ void respond_correctly_to_delete_interface(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws InterfaceNotFoundException, NamespaceNotFoundException {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockInterfaceStore).deleteInterface(namespace, 12);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/" + namespace + "/interfaces/12")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockInterfaceStore, times(1)).deleteInterface(namespace, 12);
+ }
+
+ @Test
+ void delete_interface_also_cleans_up_its_resource_mapping() throws Exception {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/interfaces/12")
+ .then()
+ .statusCode(204);
+
+ verify(mockResourceMappingStore, times(1)).deleteMappingByNumericId("valid", ResourceType.INTERFACE, 12);
+ }
+
+ @Test
+ void not_clean_up_the_resource_mapping_when_deleting_a_missing_interface() throws Exception {
+ doThrow(new InterfaceNotFoundException()).when(mockInterfaceStore).deleteInterface("valid", 12);
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/interfaces/12")
+ .then()
+ .statusCode(404);
+
+ verifyNoInteractions(mockResourceMappingStore);
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestMappingCleanupShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestMappingCleanupShould.java
new file mode 100644
index 000000000..4ab29650e
--- /dev/null
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestMappingCleanupShould.java
@@ -0,0 +1,57 @@
+package org.finos.calm.resources;
+
+import org.finos.calm.domain.ResourceType;
+import org.finos.calm.domain.exception.NamespaceNotFoundException;
+import org.finos.calm.store.ResourceMappingStore;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.slf4j.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class TestMappingCleanupShould {
+
+ @Mock
+ private ResourceMappingStore mappingStore;
+
+ @Mock
+ private Logger logger;
+
+ @Test
+ void delete_the_mapping_by_numeric_id() throws NamespaceNotFoundException {
+ MappingCleanup.deleteMapping(mappingStore, logger, "finos", ResourceType.ARCHITECTURE, 42);
+
+ verify(mappingStore).deleteMappingByNumericId("finos", ResourceType.ARCHITECTURE, 42);
+ }
+
+ @Test
+ void log_rather_than_throw_when_the_namespace_has_vanished_mid_request() throws NamespaceNotFoundException {
+ // The resource is already deleted by the time this runs, so a failure to clean up its
+ // mapping must not fail the delete response over cleanup that can't be undone anyway.
+ doThrow(new NamespaceNotFoundException())
+ .when(mappingStore).deleteMappingByNumericId("finos", ResourceType.ARCHITECTURE, 42);
+
+ assertDoesNotThrow(() ->
+ MappingCleanup.deleteMapping(mappingStore, logger, "finos", ResourceType.ARCHITECTURE, 42));
+
+ verify(mappingStore).deleteMappingByNumericId("finos", ResourceType.ARCHITECTURE, 42);
+ }
+
+ @Test
+ void log_rather_than_throw_when_the_mapping_store_fails_at_runtime() throws NamespaceNotFoundException {
+ // A driver/DB failure (MongoException, a Nitrite lock/store error, ...) must not
+ // surface as an unhandled 500 for a delete that has already succeeded.
+ doThrow(new RuntimeException("connection reset"))
+ .when(mappingStore).deleteMappingByNumericId("finos", ResourceType.ARCHITECTURE, 42);
+
+ assertDoesNotThrow(() ->
+ MappingCleanup.deleteMapping(mappingStore, logger, "finos", ResourceType.ARCHITECTURE, 42));
+
+ verify(mappingStore).deleteMappingByNumericId("finos", ResourceType.ARCHITECTURE, 42);
+ }
+}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java
index c2d8eac17..4dba94e64 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestPatternResourceShould.java
@@ -474,4 +474,62 @@ void return_forbidden_for_put_operations_on_patterns_by_default_and_when_configu
.then()
.statusCode(403);
}
+
+ @Test
+ void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_delete_pattern() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/fin_os/patterns/12")
+ .then()
+ .statusCode(400)
+ .body(containsString(NAMESPACE_MESSAGE));
+ }
+
+ static Stream provideParametersForDeletePatternTests() {
+ return Stream.of(
+ Arguments.of("invalid", new NamespaceNotFoundException(), 404),
+ Arguments.of("valid", new PatternNotFoundException(), 404),
+ Arguments.of("valid", null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeletePatternTests")
+ void respond_correctly_to_delete_pattern(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws PatternNotFoundException, NamespaceNotFoundException {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockPatternStore).deletePattern(namespace, 12);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/" + namespace + "/patterns/12")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockPatternStore, times(1)).deletePattern(namespace, 12);
+ }
+
+ @Test
+ void delete_pattern_also_cleans_up_its_resource_mapping() throws Exception {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/patterns/12")
+ .then()
+ .statusCode(204);
+
+ verify(mockResourceMappingStore, times(1)).deleteMappingByNumericId("valid", ResourceType.PATTERN, 12);
+ }
+
+ @Test
+ void not_clean_up_the_resource_mapping_when_deleting_a_missing_pattern() throws Exception {
+ doThrow(new PatternNotFoundException()).when(mockPatternStore).deletePattern("valid", 12);
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/patterns/12")
+ .then()
+ .statusCode(404);
+
+ verifyNoInteractions(mockResourceMappingStore);
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java
index 7ecfd924a..7efb1c5c5 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestStandardResourceShould.java
@@ -5,6 +5,7 @@
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
+import org.finos.calm.domain.ResourceType;
import org.finos.calm.domain.Standard;
import org.finos.calm.domain.exception.NamespaceNotFoundException;
import org.finos.calm.domain.exception.StandardNotFoundException;
@@ -349,4 +350,62 @@ void respond_correctly_to_create_standards(String namespace, Throwable exception
verify(mockStandardStore, times(1)).createStandardForVersion(createStandardRequest, namespace, 5, "1.0.1");
}
+
+ @Test
+ void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_delete_standard() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/fin_os/standards/12")
+ .then()
+ .statusCode(400)
+ .body(containsString(NAMESPACE_MESSAGE));
+ }
+
+ static Stream provideParametersForDeleteStandardTests() {
+ return Stream.of(
+ Arguments.of("invalid", new NamespaceNotFoundException(), 404),
+ Arguments.of("valid", new StandardNotFoundException(), 404),
+ Arguments.of("valid", null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteStandardTests")
+ void respond_correctly_to_delete_standard(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws StandardNotFoundException, NamespaceNotFoundException {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockStandardStore).deleteStandard(namespace, 12);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/" + namespace + "/standards/12")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockStandardStore, times(1)).deleteStandard(namespace, 12);
+ }
+
+ @Test
+ void delete_standard_also_cleans_up_its_resource_mapping() throws Exception {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/standards/12")
+ .then()
+ .statusCode(204);
+
+ verify(mockResourceMappingStore, times(1)).deleteMappingByNumericId("valid", ResourceType.STANDARD, 12);
+ }
+
+ @Test
+ void not_clean_up_the_resource_mapping_when_deleting_a_missing_standard() throws Exception {
+ doThrow(new StandardNotFoundException()).when(mockStandardStore).deleteStandard("valid", 12);
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/valid/standards/12")
+ .then()
+ .statusCode(404);
+
+ verifyNoInteractions(mockResourceMappingStore);
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java b/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java
index 747ea5c64..e3630f748 100644
--- a/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/resources/TestTimelineResourceShould.java
@@ -326,4 +326,38 @@ void return_forbidden_for_put_operations_on_timelines_by_default() {
.then()
.statusCode(403);
}
+
+ @Test
+ void return_a_400_when_an_invalid_format_of_namespace_is_provided_on_delete_timeline() {
+ given()
+ .when()
+ .delete("/api/calm/namespaces/fin_os/timelines/12")
+ .then()
+ .statusCode(400)
+ .body(containsString(NAMESPACE_MESSAGE));
+ }
+
+ static Stream provideParametersForDeleteTimelineTests() {
+ return Stream.of(
+ Arguments.of("invalid", new NamespaceNotFoundException(), 404),
+ Arguments.of("valid", new TimelineNotFoundException(), 404),
+ Arguments.of("valid", null, 204)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideParametersForDeleteTimelineTests")
+ void respond_correctly_to_delete_timeline(String namespace, Throwable exceptionToThrow, int expectedStatusCode) throws TimelineNotFoundException, NamespaceNotFoundException {
+ if (exceptionToThrow != null) {
+ doThrow(exceptionToThrow).when(mockTimelineStore).deleteTimeline(namespace, 12);
+ }
+
+ given()
+ .when()
+ .delete("/api/calm/namespaces/" + namespace + "/timelines/12")
+ .then()
+ .statusCode(expectedStatusCode);
+
+ verify(mockTimelineStore, times(1)).deleteTimeline(namespace, 12);
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/security/TestAuditRequestFilterShould.java b/calm-hub/src/test/java/org/finos/calm/security/TestAuditRequestFilterShould.java
index 4b9e62583..4e1ad1106 100644
--- a/calm-hub/src/test/java/org/finos/calm/security/TestAuditRequestFilterShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/security/TestAuditRequestFilterShould.java
@@ -429,6 +429,45 @@ void resolve_control_requirement_version_directly_from_path_params() {
assertThat(entry.getAction(), is(AuditAction.UPDATE));
}
+ @Test
+ void resolve_control_requirement_deletion_directly_from_path_params() {
+ when(resourceInfo.getResourceClass()).thenReturn((Class) ControlResource.class);
+ when(resourceInfo.getResourceMethod()).thenReturn(mockMethod(ControlResource.class, "deleteControlRequirement"));
+ MultivaluedMap pathParams = new MultivaluedHashMap<>();
+ pathParams.putSingle("domain", "payments");
+ pathParams.putSingle("controlId", "11");
+ ContainerRequestContext requestContext = mockRequest("DELETE", pathParams);
+ ContainerResponseContext responseContext = mockResponse(204, null);
+
+ filter.filter(requestContext, responseContext);
+
+ AuditLogEntry entry = captureRecordedEntry();
+ assertThat(entry.getEntityType(), is(AuditEntityType.CONTROL_REQUIREMENT));
+ assertThat(entry.getDomain(), is("payments"));
+ assertThat(entry.getEntityId(), is("11"));
+ assertThat(entry.getAction(), is(AuditAction.DELETE));
+ }
+
+ @Test
+ void resolve_control_configuration_deletion_directly_from_path_params() {
+ when(resourceInfo.getResourceClass()).thenReturn((Class) ControlResource.class);
+ when(resourceInfo.getResourceMethod()).thenReturn(mockMethod(ControlResource.class, "deleteControlConfiguration"));
+ MultivaluedMap pathParams = new MultivaluedHashMap<>();
+ pathParams.putSingle("domain", "payments");
+ pathParams.putSingle("controlId", "11");
+ pathParams.putSingle("configId", "3");
+ ContainerRequestContext requestContext = mockRequest("DELETE", pathParams);
+ ContainerResponseContext responseContext = mockResponse(204, null);
+
+ filter.filter(requestContext, responseContext);
+
+ AuditLogEntry entry = captureRecordedEntry();
+ assertThat(entry.getEntityType(), is(AuditEntityType.CONTROL_CONFIGURATION));
+ assertThat(entry.getDomain(), is("payments"));
+ assertThat(entry.getEntityId(), is("3"));
+ assertThat(entry.getAction(), is(AuditAction.DELETE));
+ }
+
// --- MappingControllerResource dispatch --------------------------------------
@Test
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java
index 385b65bc6..6021b7314 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoAdrStoreShould.java
@@ -8,6 +8,7 @@
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -455,4 +456,30 @@ void report_the_revision_already_exists_when_two_writers_race() throws Exception
assertThrows(AdrRevisionExistsException.class,
() -> store.updateAdrStatus(adrMeta(1, null), Status.accepted));
}
+
+ // --- deleteAdr ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_an_adr_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteAdr(NAMESPACE, ADR_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_revisions_when_the_adr_exists() throws Exception {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteAdr(NAMESPACE, ADR_ID);
+
+ verify(versionCollection).deleteMany(any(Bson.class));
+ verify(headerCollection).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void throw_an_adr_exception_when_deleting_a_missing_adr() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThrows(AdrNotFoundException.class, () -> store.deleteAdr(NAMESPACE, ADR_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoArchitectureStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoArchitectureStoreShould.java
index 227913340..9e32c04f7 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoArchitectureStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoArchitectureStoreShould.java
@@ -7,6 +7,7 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -431,6 +432,32 @@ void report_a_plain_write_failure_for_other_version_write_errors() {
assertThat(exception.isCapacityExceeded(), is(false));
}
+ // --- deleteArchitecture ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_an_architecture_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteArchitecture(NAMESPACE, ARCHITECTURE_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_versions_when_the_architecture_exists() throws Exception {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteArchitecture(NAMESPACE, ARCHITECTURE_ID);
+
+ verify(versionCollection).deleteMany(any(Bson.class));
+ verify(headerCollection).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void throw_an_architecture_exception_when_deleting_a_missing_architecture() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThrows(ArchitectureNotFoundException.class, () -> store.deleteArchitecture(NAMESPACE, ARCHITECTURE_ID));
+ }
+
@Test
void page_the_summary_window_at_the_database() throws NamespaceNotFoundException {
FindIterable iterable = stubFind(headerCollection, List.of());
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoControlStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoControlStoreShould.java
index 027010ed5..7c5555995 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoControlStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoControlStoreShould.java
@@ -6,6 +6,7 @@
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -19,6 +20,7 @@
import org.finos.calm.domain.exception.ControlConfigurationNotFoundException;
import org.finos.calm.domain.exception.ControlConfigurationVersionExistsException;
import org.finos.calm.domain.exception.ControlConfigurationVersionNotFoundException;
+import org.finos.calm.domain.exception.ControlHasConfigurationsException;
import org.finos.calm.domain.exception.ControlNotFoundException;
import org.finos.calm.domain.exception.ControlRequirementVersionExistsException;
import org.finos.calm.domain.exception.ControlRequirementVersionNotFoundException;
@@ -38,6 +40,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -581,4 +584,78 @@ void create_configuration_for_version_never_syncs_name_or_description_onto_the_h
verify(configHeaders, Mockito.times(1)).updateOne(any(Bson.class), any(Bson.class));
}
+
+ // --- deleteControlRequirement ---
+
+ @Test
+ void throw_a_domain_exception_when_deleting_a_control_requirement_in_a_missing_domain() {
+ assertThrows(DomainNotFoundException.class, () -> store.deleteControlRequirement("invalid", CONTROL_ID));
+ }
+
+ @Test
+ void throw_a_control_exception_when_deleting_a_missing_control_requirement() {
+ controlDoesNotExist();
+
+ assertThrows(ControlNotFoundException.class, () -> store.deleteControlRequirement(DOMAIN, CONTROL_ID));
+ }
+
+ @Test
+ void refuse_to_delete_a_control_requirement_that_still_has_configurations() {
+ controlExists();
+ when(configHeaders.countDocuments(any(Bson.class))).thenReturn(2L);
+
+ ControlHasConfigurationsException exception = assertThrows(ControlHasConfigurationsException.class,
+ () -> store.deleteControlRequirement(DOMAIN, CONTROL_ID));
+ assertThat(exception.getControlId(), is(CONTROL_ID));
+ assertThat(exception.getConfigurationCount(), is(2));
+ verify(controlHeaders, never()).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void delete_the_requirement_header_and_all_versions_when_it_has_no_configurations() throws Exception {
+ controlExists();
+ when(configHeaders.countDocuments(any(Bson.class))).thenReturn(0L);
+ when(controlHeaders.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteControlRequirement(DOMAIN, CONTROL_ID);
+
+ verify(controlVersions).deleteMany(any(Bson.class));
+ verify(controlHeaders).deleteOne(any(Bson.class));
+ }
+
+ // --- deleteControlConfiguration ---
+
+ @Test
+ void throw_a_domain_exception_when_deleting_a_configuration_in_a_missing_domain() {
+ assertThrows(DomainNotFoundException.class,
+ () -> store.deleteControlConfiguration("invalid", CONTROL_ID, CONFIGURATION_ID));
+ }
+
+ @Test
+ void throw_a_control_exception_when_deleting_a_configuration_for_a_missing_control() {
+ controlDoesNotExist();
+
+ assertThrows(ControlNotFoundException.class,
+ () -> store.deleteControlConfiguration(DOMAIN, CONTROL_ID, CONFIGURATION_ID));
+ }
+
+ @Test
+ void throw_a_configuration_exception_when_deleting_a_missing_configuration() {
+ controlExists();
+ stubFind(configHeaders, List.of());
+
+ assertThrows(ControlConfigurationNotFoundException.class,
+ () -> store.deleteControlConfiguration(DOMAIN, CONTROL_ID, CONFIGURATION_ID));
+ }
+
+ @Test
+ void delete_the_configuration_header_and_all_versions_when_it_exists() throws Exception {
+ configurationExists();
+ when(configHeaders.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteControlConfiguration(DOMAIN, CONTROL_ID, CONFIGURATION_ID);
+
+ verify(configVersions).deleteMany(any(Bson.class));
+ verify(configHeaders).deleteOne(any(Bson.class));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoDecoratorStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoDecoratorStoreShould.java
index 31ba3de82..85f9c0452 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoDecoratorStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoDecoratorStoreShould.java
@@ -728,4 +728,58 @@ void should_throw_namespace_not_found_when_updating_decorator_in_unknown_namespa
verify(namespaceStore).namespaceExists(namespace);
verify(decoratorCollection, never()).updateOne(any(Bson.class), any(Bson.class));
}
+
+ @Test
+ void should_delete_decorator_successfully() throws Exception {
+ // Given
+ String namespace = "finos";
+ int decoratorId = 1;
+
+ when(namespaceStore.namespaceExists(namespace)).thenReturn(true);
+
+ UpdateResult updateResult = mock(UpdateResult.class);
+ when(updateResult.getModifiedCount()).thenReturn(1L);
+ when(decoratorCollection.updateOne(any(Bson.class), any(Bson.class))).thenReturn(updateResult);
+
+ // When
+ decoratorStore.deleteDecorator(namespace, decoratorId);
+
+ // Then
+ verify(namespaceStore).namespaceExists(namespace);
+ verify(decoratorCollection).updateOne(any(Bson.class), any(Bson.class));
+ }
+
+ @Test
+ void should_throw_decorator_not_found_when_delete_matches_nothing() {
+ // Given
+ String namespace = "finos";
+ int decoratorId = 99;
+
+ when(namespaceStore.namespaceExists(namespace)).thenReturn(true);
+
+ UpdateResult updateResult = mock(UpdateResult.class);
+ when(updateResult.getModifiedCount()).thenReturn(0L);
+ when(decoratorCollection.updateOne(any(Bson.class), any(Bson.class))).thenReturn(updateResult);
+
+ // When & Then
+ assertThrows(DecoratorNotFoundException.class,
+ () -> decoratorStore.deleteDecorator(namespace, decoratorId));
+
+ verify(namespaceStore).namespaceExists(namespace);
+ verify(decoratorCollection).updateOne(any(Bson.class), any(Bson.class));
+ }
+
+ @Test
+ void should_throw_namespace_not_found_when_deleting_decorator_in_unknown_namespace() {
+ // Given
+ String namespace = "unknown-namespace";
+ when(namespaceStore.namespaceExists(namespace)).thenReturn(false);
+
+ // When & Then
+ assertThrows(NamespaceNotFoundException.class,
+ () -> decoratorStore.deleteDecorator(namespace, 1));
+
+ verify(namespaceStore).namespaceExists(namespace);
+ verify(decoratorCollection, never()).updateOne(any(Bson.class), any(Bson.class));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoFlowStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoFlowStoreShould.java
index a66401bca..0db7f214d 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoFlowStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoFlowStoreShould.java
@@ -7,6 +7,7 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -412,4 +413,30 @@ void report_a_plain_write_failure_for_other_version_write_errors() {
() -> store.updateFlowForVersion(flow("1.0.1")));
assertThat(exception.isCapacityExceeded(), is(false));
}
+
+ // --- deleteFlow ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_a_flow_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteFlow(NAMESPACE, FLOW_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_versions_when_the_flow_exists() throws Exception {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteFlow(NAMESPACE, FLOW_ID);
+
+ verify(versionCollection).deleteMany(any(Bson.class));
+ verify(headerCollection).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void throw_a_flow_exception_when_deleting_a_missing_flow() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThrows(FlowNotFoundException.class, () -> store.deleteFlow(NAMESPACE, FLOW_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoInterfaceStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoInterfaceStoreShould.java
index 3d293fcc6..5ca717515 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoInterfaceStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoInterfaceStoreShould.java
@@ -6,6 +6,7 @@
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -294,4 +295,30 @@ void overwrite_the_header_details_even_when_blank() throws Exception {
// which guarded them. Preserved rather than harmonised — see the store's javadoc.
verify(headerCollection, Mockito.times(2)).updateOne(any(Bson.class), any(Bson.class));
}
+
+ // --- deleteInterface ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_an_interface_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteInterface(NAMESPACE, INTERFACE_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_versions_when_the_interface_exists() throws Exception {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteInterface(NAMESPACE, INTERFACE_ID);
+
+ verify(versionCollection).deleteMany(any(Bson.class));
+ verify(headerCollection).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void throw_an_interface_exception_when_deleting_a_missing_interface() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThrows(InterfaceNotFoundException.class, () -> store.deleteInterface(NAMESPACE, INTERFACE_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoPatternStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoPatternStoreShould.java
index 5c6ef6d0b..72bd0c527 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoPatternStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoPatternStoreShould.java
@@ -7,6 +7,7 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -178,6 +179,32 @@ void fall_back_to_a_generated_name_for_headers_missing_one() throws NamespaceNot
new NamespaceResourceSummary("Pattern 7", "", 7, 0)));
}
+ // --- deletePattern ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_a_pattern_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deletePattern(NAMESPACE, PATTERN_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_versions_when_the_pattern_exists() throws Exception {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deletePattern(NAMESPACE, PATTERN_ID);
+
+ verify(versionCollection).deleteMany(any(Bson.class));
+ verify(headerCollection).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void throw_a_pattern_exception_when_deleting_a_missing_pattern() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThrows(PatternNotFoundException.class, () -> store.deletePattern(NAMESPACE, PATTERN_ID));
+ }
+
@Test
void page_the_summary_window_at_the_database() throws NamespaceNotFoundException {
FindIterable iterable = stubFind(headerCollection, List.of());
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoResourceMappingStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoResourceMappingStoreShould.java
index 737090354..6967e2c26 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoResourceMappingStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoResourceMappingStoreShould.java
@@ -365,6 +365,44 @@ void throw_namespace_not_found_on_delete_when_namespace_does_not_exist() {
() -> store.deleteMapping("invalid", ResourceType.PATTERN, "test"));
}
+ // --- deleteMappingByNumericId ---
+
+ @Test
+ void delete_mapping_by_numeric_id_successfully() throws NamespaceNotFoundException {
+ when(namespaceStore.namespaceExists("finos")).thenReturn(true);
+
+ var deleteResult = Mockito.mock(com.mongodb.client.result.DeleteResult.class);
+ when(deleteResult.getDeletedCount()).thenReturn(1L);
+ when(mappingCollection.deleteOne(any(org.bson.conversions.Bson.class))).thenReturn(deleteResult);
+
+ store.deleteMappingByNumericId("finos", ResourceType.PATTERN, 42);
+
+ verify(mappingCollection).deleteOne(any(org.bson.conversions.Bson.class));
+ }
+
+ @Test
+ void not_throw_when_deleting_a_mapping_by_numeric_id_that_does_not_exist() throws NamespaceNotFoundException {
+ // Most resources have no custom-id mapping at all — this must be a silent no-op, not
+ // an error, so a resource delete can call it unconditionally.
+ when(namespaceStore.namespaceExists("finos")).thenReturn(true);
+
+ var deleteResult = Mockito.mock(com.mongodb.client.result.DeleteResult.class);
+ when(deleteResult.getDeletedCount()).thenReturn(0L);
+ when(mappingCollection.deleteOne(any(org.bson.conversions.Bson.class))).thenReturn(deleteResult);
+
+ store.deleteMappingByNumericId("finos", ResourceType.PATTERN, 999);
+
+ verify(mappingCollection).deleteOne(any(org.bson.conversions.Bson.class));
+ }
+
+ @Test
+ void throw_namespace_not_found_when_deleting_a_mapping_by_numeric_id_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists("invalid")).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class,
+ () -> store.deleteMappingByNumericId("invalid", ResourceType.PATTERN, 42));
+ }
+
// --- Helper interfaces for Mockito generics ---
private interface DocumentMongoCollection extends MongoCollection {
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoStandardStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoStandardStoreShould.java
index dfc7703cb..c8a64ddd4 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoStandardStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoStandardStoreShould.java
@@ -6,6 +6,7 @@
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -294,4 +295,30 @@ void overwrite_the_header_details_even_when_blank() throws Exception {
// which guarded them. Preserved rather than harmonised — see the store's javadoc.
verify(headerCollection, Mockito.times(2)).updateOne(any(Bson.class), any(Bson.class));
}
+
+ // --- deleteStandard ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_a_standard_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteStandard(NAMESPACE, STANDARD_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_versions_when_the_standard_exists() throws Exception {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteStandard(NAMESPACE, STANDARD_ID);
+
+ verify(versionCollection).deleteMany(any(Bson.class));
+ verify(headerCollection).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void throw_a_standard_exception_when_deleting_a_missing_standard() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThrows(StandardNotFoundException.class, () -> store.deleteStandard(NAMESPACE, STANDARD_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoTimelineStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoTimelineStoreShould.java
index 971051c43..6b0f760b9 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoTimelineStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/mongo/TestMongoTimelineStoreShould.java
@@ -7,6 +7,7 @@
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
@@ -412,4 +413,30 @@ void report_a_plain_write_failure_for_other_version_write_errors() {
() -> store.updateTimelineForVersion(timeline("1.0.1")));
assertThat(exception.isCapacityExceeded(), is(false));
}
+
+ // --- deleteTimeline ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_a_timeline_in_a_missing_namespace() {
+ when(namespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteTimeline(NAMESPACE, TIMELINE_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_versions_when_the_timeline_exists() throws Exception {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ store.deleteTimeline(NAMESPACE, TIMELINE_ID);
+
+ verify(versionCollection).deleteMany(any(Bson.class));
+ verify(headerCollection).deleteOne(any(Bson.class));
+ }
+
+ @Test
+ void throw_a_timeline_exception_when_deleting_a_missing_timeline() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThrows(TimelineNotFoundException.class, () -> store.deleteTimeline(NAMESPACE, TIMELINE_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteAdrStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteAdrStoreShould.java
index e0deb388d..0cf931f63 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteAdrStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteAdrStoreShould.java
@@ -382,4 +382,33 @@ public void report_the_revision_already_exists_when_two_writers_race() throws Ex
assertThrows(AdrRevisionExistsException.class,
() -> store.updateAdrStatus(adrMeta(1, null), Status.accepted));
}
+
+ // --- deleteAdr ---
+
+ @Test
+ void throw_a_namespace_exception_when_deleting_an_adr_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteAdr(NAMESPACE, ADR_ID));
+ }
+
+ @Test
+ void delete_the_header_and_all_revisions_when_the_adr_exists() throws Exception {
+ adrExists();
+ stubFind(versionCollection, List.of(
+ Document.createDocument().put("version", "1"),
+ Document.createDocument().put("version", "2")));
+
+ store.deleteAdr(NAMESPACE, ADR_ID);
+
+ verify(versionCollection, org.mockito.Mockito.times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(any(Document.class));
+ }
+
+ @Test
+ void throw_an_adr_exception_when_deleting_a_missing_adr() {
+ adrDoesNotExist();
+
+ assertThrows(AdrNotFoundException.class, () -> store.deleteAdr(NAMESPACE, ADR_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteArchitectureStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteArchitectureStoreShould.java
index 169d5d23a..454a3f491 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteArchitectureStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteArchitectureStoreShould.java
@@ -428,4 +428,33 @@ public void create_the_version_when_updating_one_that_does_not_exist() throws Ex
verify(versionCollection).insert(any(Document.class));
}
+
+ // --- deleteArchitecture ---
+
+ @Test
+ public void throw_a_namespace_exception_when_deleting_an_architecture_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteArchitecture(NAMESPACE, ARCHITECTURE_ID));
+ }
+
+ @Test
+ public void delete_the_header_and_all_versions_when_the_architecture_exists() throws Exception {
+ architectureExists();
+ stubFind(versionCollection, List.of(
+ Document.createDocument().put("version", "1.0.0"),
+ Document.createDocument().put("version", "1.0.1")));
+
+ store.deleteArchitecture(NAMESPACE, ARCHITECTURE_ID);
+
+ verify(versionCollection, org.mockito.Mockito.times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(any(Document.class));
+ }
+
+ @Test
+ public void throw_an_architecture_exception_when_deleting_a_missing_architecture() {
+ architectureDoesNotExist();
+
+ assertThrows(ArchitectureNotFoundException.class, () -> store.deleteArchitecture(NAMESPACE, ARCHITECTURE_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteControlStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteControlStoreShould.java
index f870fa53b..638a3576c 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteControlStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteControlStoreShould.java
@@ -12,6 +12,7 @@
import org.finos.calm.domain.exception.ControlConfigurationNotFoundException;
import org.finos.calm.domain.exception.ControlConfigurationVersionExistsException;
import org.finos.calm.domain.exception.ControlConfigurationVersionNotFoundException;
+import org.finos.calm.domain.exception.ControlHasConfigurationsException;
import org.finos.calm.domain.exception.ControlNotFoundException;
import org.finos.calm.domain.exception.ControlRequirementVersionExistsException;
import org.finos.calm.domain.exception.ControlRequirementVersionNotFoundException;
@@ -32,6 +33,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -113,6 +115,13 @@ private void configurationDoesNotExist() {
stubFind(configHeaders, List.of());
}
+ /** Stubs the count query {@code countHeaders} runs against the configuration namespace. */
+ private void configurationCountIs(long count) {
+ DocumentCursor countCursor = mock(DocumentCursor.class);
+ when(configHeaders.find(any(Filter.class))).thenReturn(countCursor);
+ when(countCursor.size()).thenReturn(count);
+ }
+
// --- getControlsForDomain ---
@Test
@@ -502,4 +511,73 @@ void create_configuration_for_version_never_syncs_name_or_description_onto_the_h
verify(configHeaders, times(1)).update(any(Filter.class), any(Document.class));
}
+
+ // --- deleteControlRequirement ---
+
+ @Test
+ void throw_a_domain_exception_when_deleting_a_control_requirement_in_a_missing_domain() {
+ assertThrows(DomainNotFoundException.class, () -> store.deleteControlRequirement("invalid", CONTROL_ID));
+ }
+
+ @Test
+ void throw_a_control_exception_when_deleting_a_missing_control_requirement() {
+ controlDoesNotExist();
+
+ assertThrows(ControlNotFoundException.class, () -> store.deleteControlRequirement(DOMAIN, CONTROL_ID));
+ }
+
+ @Test
+ void refuse_to_delete_a_control_requirement_that_still_has_configurations() {
+ controlExists();
+ configurationCountIs(2);
+
+ ControlHasConfigurationsException exception = assertThrows(ControlHasConfigurationsException.class,
+ () -> store.deleteControlRequirement(DOMAIN, CONTROL_ID));
+ assertThat(exception.getControlId(), is(CONTROL_ID));
+ assertThat(exception.getConfigurationCount(), is(2));
+ verify(controlHeaders, never()).remove(any(Document.class));
+ }
+
+ @Test
+ void delete_the_requirement_header_and_all_versions_when_it_has_no_configurations() throws Exception {
+ controlExists();
+ configurationCountIs(0);
+
+ store.deleteControlRequirement(DOMAIN, CONTROL_ID);
+
+ verify(controlHeaders).remove(any(Document.class));
+ }
+
+ // --- deleteControlConfiguration ---
+
+ @Test
+ void throw_a_domain_exception_when_deleting_a_configuration_in_a_missing_domain() {
+ assertThrows(DomainNotFoundException.class,
+ () -> store.deleteControlConfiguration("invalid", CONTROL_ID, CONFIGURATION_ID));
+ }
+
+ @Test
+ void throw_a_control_exception_when_deleting_a_configuration_for_a_missing_control() {
+ controlDoesNotExist();
+
+ assertThrows(ControlNotFoundException.class,
+ () -> store.deleteControlConfiguration(DOMAIN, CONTROL_ID, CONFIGURATION_ID));
+ }
+
+ @Test
+ void throw_a_configuration_exception_when_deleting_a_missing_configuration() {
+ configurationDoesNotExist();
+
+ assertThrows(ControlConfigurationNotFoundException.class,
+ () -> store.deleteControlConfiguration(DOMAIN, CONTROL_ID, CONFIGURATION_ID));
+ }
+
+ @Test
+ void delete_the_configuration_header_and_all_versions_when_it_exists() throws Exception {
+ configurationExists();
+
+ store.deleteControlConfiguration(DOMAIN, CONTROL_ID, CONFIGURATION_ID);
+
+ verify(configHeaders).remove(any(Document.class));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteDecoratorStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteDecoratorStoreShould.java
index 33c259e96..89a1a21f8 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteDecoratorStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteDecoratorStoreShould.java
@@ -678,4 +678,74 @@ void should_handle_multiple_decorators_in_order() throws NamespaceNotFoundExcept
assertEquals(1, decoratorIds.get(2));
verify(namespaceStore).namespaceExists(namespace);
}
+
+ @Test
+ void should_delete_decorator_successfully() throws NamespaceNotFoundException, DecoratorNotFoundException {
+ // Given
+ String namespace = "finos";
+ when(namespaceStore.namespaceExists(namespace)).thenReturn(true);
+
+ Document decorator1 = Document.createDocument("decoratorId", 1)
+ .put("decorator", Document.createDocument("unique-id", "decorator-1"));
+ Document decorator2 = Document.createDocument("decoratorId", 2)
+ .put("decorator", Document.createDocument("unique-id", "decorator-2"));
+ Document namespaceDocument = Document.createDocument("namespace", namespace)
+ .put("decorators", List.of(decorator1, decorator2));
+
+ when(decoratorCollection.find(any(Filter.class))).thenReturn(cursor);
+ when(cursor.firstOrNull()).thenReturn(namespaceDocument);
+
+ // When
+ decoratorStore.deleteDecorator(namespace, 1);
+
+ // Then
+ verify(namespaceStore).namespaceExists(namespace);
+ verify(decoratorCollection).update(any(Document.class));
+ }
+
+ @Test
+ void should_throw_decorator_not_found_when_deleting_an_id_not_present() {
+ // Given
+ String namespace = "finos";
+ when(namespaceStore.namespaceExists(namespace)).thenReturn(true);
+
+ Document decorator1 = Document.createDocument("decoratorId", 1)
+ .put("decorator", Document.createDocument("unique-id", "decorator-1"));
+ Document namespaceDocument = Document.createDocument("namespace", namespace)
+ .put("decorators", List.of(decorator1));
+
+ when(decoratorCollection.find(any(Filter.class))).thenReturn(cursor);
+ when(cursor.firstOrNull()).thenReturn(namespaceDocument);
+
+ // When & Then
+ assertThrows(DecoratorNotFoundException.class, () -> decoratorStore.deleteDecorator(namespace, 99));
+
+ verify(decoratorCollection, never()).update(any(Document.class));
+ }
+
+ @Test
+ void should_throw_decorator_not_found_when_deleting_from_a_namespace_with_no_document() {
+ // Given
+ String namespace = "finos";
+ when(namespaceStore.namespaceExists(namespace)).thenReturn(true);
+ when(decoratorCollection.find(any(Filter.class))).thenReturn(cursor);
+ when(cursor.firstOrNull()).thenReturn(null);
+
+ // When & Then
+ assertThrows(DecoratorNotFoundException.class, () -> decoratorStore.deleteDecorator(namespace, 1));
+
+ verify(decoratorCollection, never()).update(any(Document.class));
+ }
+
+ @Test
+ void should_throw_namespace_not_found_when_deleting_decorator_in_unknown_namespace() {
+ // Given
+ String namespace = "unknown-namespace";
+ when(namespaceStore.namespaceExists(namespace)).thenReturn(false);
+
+ // When & Then
+ assertThrows(NamespaceNotFoundException.class, () -> decoratorStore.deleteDecorator(namespace, 1));
+
+ verify(decoratorCollection, never()).find(any(Filter.class));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteFlowStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteFlowStoreShould.java
index c056f6313..349db831e 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteFlowStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteFlowStoreShould.java
@@ -389,4 +389,33 @@ public void create_the_version_when_updating_one_that_does_not_exist() throws Ex
verify(versionCollection).insert(any(Document.class));
}
+
+ // --- deleteFlow ---
+
+ @Test
+ public void throw_a_namespace_exception_when_deleting_a_flow_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteFlow(NAMESPACE, FLOW_ID));
+ }
+
+ @Test
+ public void delete_the_header_and_all_versions_when_the_flow_exists() throws Exception {
+ flowExists();
+ stubFind(versionCollection, List.of(
+ Document.createDocument().put("version", "1.0.0"),
+ Document.createDocument().put("version", "1.0.1")));
+
+ store.deleteFlow(NAMESPACE, FLOW_ID);
+
+ verify(versionCollection, org.mockito.Mockito.times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(any(Document.class));
+ }
+
+ @Test
+ public void throw_a_flow_exception_when_deleting_a_missing_flow() {
+ flowDoesNotExist();
+
+ assertThrows(FlowNotFoundException.class, () -> store.deleteFlow(NAMESPACE, FLOW_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteInterfaceStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteInterfaceStoreShould.java
index ed5e48c7a..4c26068df 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteInterfaceStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteInterfaceStoreShould.java
@@ -263,4 +263,33 @@ public void overwrite_the_header_details_even_when_blank() throws Exception {
// Unconditional, unlike Pattern and Flow — Interface's old shape did not guard these.
verify(headerCollection, times(2)).update(any(Filter.class), any(Document.class));
}
+
+ // --- deleteInterface ---
+
+ @Test
+ public void throw_a_namespace_exception_when_deleting_an_interface_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteInterface(NAMESPACE, INTERFACE_ID));
+ }
+
+ @Test
+ public void delete_the_header_and_all_versions_when_the_interface_exists() throws Exception {
+ interfaceExists();
+ stubFind(versionCollection, List.of(
+ Document.createDocument().put("version", "1.0.0"),
+ Document.createDocument().put("version", "1.0.1")));
+
+ store.deleteInterface(NAMESPACE, INTERFACE_ID);
+
+ verify(versionCollection, times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(any(Document.class));
+ }
+
+ @Test
+ public void throw_an_interface_exception_when_deleting_a_missing_interface() {
+ interfaceDoesNotExist();
+
+ assertThrows(InterfaceNotFoundException.class, () -> store.deleteInterface(NAMESPACE, INTERFACE_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitritePatternStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitritePatternStoreShould.java
index 114445775..e0787b0d4 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitritePatternStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitritePatternStoreShould.java
@@ -431,4 +431,33 @@ public void create_the_version_when_updating_one_that_does_not_exist() throws Ex
verify(versionCollection).insert(any(Document.class));
}
+
+ // --- deletePattern ---
+
+ @Test
+ public void throw_a_namespace_exception_when_deleting_a_pattern_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deletePattern(NAMESPACE, PATTERN_ID));
+ }
+
+ @Test
+ public void delete_the_header_and_all_versions_when_the_pattern_exists() throws Exception {
+ patternExists();
+ stubFind(versionCollection, List.of(
+ Document.createDocument().put("version", "1.0.0"),
+ Document.createDocument().put("version", "1.0.1")));
+
+ store.deletePattern(NAMESPACE, PATTERN_ID);
+
+ verify(versionCollection, org.mockito.Mockito.times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(any(Document.class));
+ }
+
+ @Test
+ public void throw_a_pattern_exception_when_deleting_a_missing_pattern() {
+ patternDoesNotExist();
+
+ assertThrows(PatternNotFoundException.class, () -> store.deletePattern(NAMESPACE, PATTERN_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteResourceMappingStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteResourceMappingStoreShould.java
index 986a291ab..27540c717 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteResourceMappingStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteResourceMappingStoreShould.java
@@ -410,4 +410,48 @@ void throw_mapping_not_found_on_delete_when_mapping_does_not_exist() {
assertThrows(MappingNotFoundException.class,
() -> store.deleteMapping(NAMESPACE, ResourceType.PATTERN, "nonexistent"));
}
+
+ // --- deleteMappingByNumericId ---
+
+ @Test
+ void delete_mapping_by_numeric_id_successfully() throws NamespaceNotFoundException {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(true);
+
+ Document existing = Document.createDocument()
+ .put("namespace", NAMESPACE)
+ .put("customId", "api-gateway")
+ .put("resourceType", "PATTERN")
+ .put("numericId", 42);
+
+ DocumentCursor cursor = mock(DocumentCursor.class);
+ when(cursor.firstOrNull()).thenReturn(existing);
+ when(mockCollection.find(any(Filter.class))).thenReturn(cursor);
+
+ store.deleteMappingByNumericId(NAMESPACE, ResourceType.PATTERN, 42);
+
+ verify(mockCollection).remove(existing);
+ }
+
+ @Test
+ void not_throw_when_deleting_a_mapping_by_numeric_id_that_does_not_exist() throws NamespaceNotFoundException {
+ // Most resources have no custom-id mapping at all — this must be a silent no-op, not
+ // an error, so a resource delete can call it unconditionally.
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(true);
+
+ DocumentCursor cursor = mock(DocumentCursor.class);
+ when(cursor.firstOrNull()).thenReturn(null);
+ when(mockCollection.find(any(Filter.class))).thenReturn(cursor);
+
+ store.deleteMappingByNumericId(NAMESPACE, ResourceType.PATTERN, 999);
+
+ verify(mockCollection, never()).remove(any(Document.class));
+ }
+
+ @Test
+ void throw_namespace_not_found_when_deleting_a_mapping_by_numeric_id_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists("invalid")).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class,
+ () -> store.deleteMappingByNumericId("invalid", ResourceType.PATTERN, 42));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteStandardStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteStandardStoreShould.java
index 6024b8e64..358dea793 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteStandardStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteStandardStoreShould.java
@@ -263,4 +263,33 @@ public void overwrite_the_header_details_even_when_blank() throws Exception {
// Unconditional, unlike Pattern and Flow — Standard's old shape did not guard these.
verify(headerCollection, times(2)).update(any(Filter.class), any(Document.class));
}
+
+ // --- deleteStandard ---
+
+ @Test
+ public void throw_a_namespace_exception_when_deleting_a_standard_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteStandard(NAMESPACE, STANDARD_ID));
+ }
+
+ @Test
+ public void delete_the_header_and_all_versions_when_the_standard_exists() throws Exception {
+ standardExists();
+ stubFind(versionCollection, List.of(
+ Document.createDocument().put("version", "1.0.0"),
+ Document.createDocument().put("version", "1.0.1")));
+
+ store.deleteStandard(NAMESPACE, STANDARD_ID);
+
+ verify(versionCollection, times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(any(Document.class));
+ }
+
+ @Test
+ public void throw_a_standard_exception_when_deleting_a_missing_standard() {
+ standardDoesNotExist();
+
+ assertThrows(StandardNotFoundException.class, () -> store.deleteStandard(NAMESPACE, STANDARD_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteTimelineStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteTimelineStoreShould.java
index df5af265d..10e5a0166 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteTimelineStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/nitrite/TestNitriteTimelineStoreShould.java
@@ -389,4 +389,33 @@ public void create_the_version_when_updating_one_that_does_not_exist() throws Ex
verify(versionCollection).insert(any(Document.class));
}
+
+ // --- deleteTimeline ---
+
+ @Test
+ public void throw_a_namespace_exception_when_deleting_a_timeline_in_a_missing_namespace() {
+ when(mockNamespaceStore.namespaceExists(NAMESPACE)).thenReturn(false);
+
+ assertThrows(NamespaceNotFoundException.class, () -> store.deleteTimeline(NAMESPACE, TIMELINE_ID));
+ }
+
+ @Test
+ public void delete_the_header_and_all_versions_when_the_timeline_exists() throws Exception {
+ timelineExists();
+ stubFind(versionCollection, List.of(
+ Document.createDocument().put("version", "1.0.0"),
+ Document.createDocument().put("version", "1.0.1")));
+
+ store.deleteTimeline(NAMESPACE, TIMELINE_ID);
+
+ verify(versionCollection, org.mockito.Mockito.times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(any(Document.class));
+ }
+
+ @Test
+ public void throw_a_timeline_exception_when_deleting_a_missing_timeline() {
+ timelineDoesNotExist();
+
+ assertThrows(TimelineNotFoundException.class, () -> store.deleteTimeline(NAMESPACE, TIMELINE_ID));
+ }
}
diff --git a/calm-hub/src/test/java/org/finos/calm/store/util/TestMongoVersionDocumentStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/util/TestMongoVersionDocumentStoreShould.java
index efe03ab6b..e2576c426 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/util/TestMongoVersionDocumentStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/util/TestMongoVersionDocumentStoreShould.java
@@ -7,6 +7,7 @@
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.bson.BsonDocument;
import org.bson.BsonObjectId;
@@ -179,6 +180,39 @@ void swallow_a_failure_to_delete_a_header() {
assertDoesNotThrow(() -> store.deleteHeader(NAMESPACE, RESOURCE_ID));
}
+ // --- deleteResource ---
+
+ @Test
+ void delete_the_header_and_all_versions_of_a_resource() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(1));
+
+ boolean deleted = store.deleteResource(NAMESPACE, RESOURCE_ID);
+
+ assertThat(deleted, is(true));
+ ArgumentCaptor versionFilterCaptor = ArgumentCaptor.forClass(Bson.class);
+ verify(versionCollection).deleteMany(versionFilterCaptor.capture());
+ assertThat(asJson(versionFilterCaptor.getValue()), containsString(NAMESPACE));
+ ArgumentCaptor headerFilterCaptor = ArgumentCaptor.forClass(Bson.class);
+ verify(headerCollection).deleteOne(headerFilterCaptor.capture());
+ assertThat(asJson(headerFilterCaptor.getValue()), containsString(NAMESPACE));
+ }
+
+ @Test
+ void report_false_when_deleting_a_resource_that_does_not_exist() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenReturn(DeleteResult.acknowledged(0));
+
+ assertThat(store.deleteResource(NAMESPACE, RESOURCE_ID), is(false));
+ }
+
+ @Test
+ void translate_a_write_failure_when_deleting_a_resource() {
+ when(headerCollection.deleteOne(any(Bson.class))).thenThrow(new MongoTimeoutException("no server"));
+
+ // Unlike deleteHeader, this backs a real DELETE endpoint — a failure here must be
+ // reported, not swallowed as best-effort cleanup.
+ assertThrows(StorageWriteException.class, () -> store.deleteResource(NAMESPACE, RESOURCE_ID));
+ }
+
// --- createVersion ---
@Test
diff --git a/calm-hub/src/test/java/org/finos/calm/store/util/TestNitriteVersionDocumentStoreShould.java b/calm-hub/src/test/java/org/finos/calm/store/util/TestNitriteVersionDocumentStoreShould.java
index fdaa2678c..b5ea0c77e 100644
--- a/calm-hub/src/test/java/org/finos/calm/store/util/TestNitriteVersionDocumentStoreShould.java
+++ b/calm-hub/src/test/java/org/finos/calm/store/util/TestNitriteVersionDocumentStoreShould.java
@@ -28,6 +28,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@@ -125,6 +126,30 @@ void swallow_a_failure_to_delete_a_header() {
assertDoesNotThrow(() -> store.deleteHeader(NAMESPACE, RESOURCE_ID));
}
+ // --- deleteResource ---
+
+ @Test
+ void delete_the_header_and_all_versions_of_a_resource() {
+ Document header = header(RESOURCE_ID, "name", "description", 2);
+ stubFind(headerCollection, List.of(header));
+ stubFind(versionCollection, List.of(versionDocument("1.0.0"), versionDocument("2.0.0")));
+
+ boolean deleted = store.deleteResource(NAMESPACE, RESOURCE_ID);
+
+ assertThat(deleted, is(true));
+ verify(versionCollection, times(2)).remove(any(Document.class));
+ verify(headerCollection).remove(header);
+ }
+
+ @Test
+ void report_false_when_deleting_a_resource_that_does_not_exist() {
+ stubFind(headerCollection, List.of());
+
+ assertThat(store.deleteResource(NAMESPACE, RESOURCE_ID), is(false));
+ verify(headerCollection, never()).remove(any(Document.class));
+ verifyNoInteractions(versionCollection);
+ }
+
// --- createVersion ---
@Test