|
|
@@ -37,6 +37,8 @@ using smartbotic::database::RelationBlock;
|
|
|
using smartbotic::database::RelationEnforcer;
|
|
|
using smartbotic::database::RelationInfo;
|
|
|
using smartbotic::database::RelationManager;
|
|
|
+using smartbotic::database::RelationImpact;
|
|
|
+using smartbotic::database::describeDeleteImpacts;
|
|
|
using smartbotic::database::findRelationBlocks;
|
|
|
using smartbotic::db::storage::LmdbDocumentStore;
|
|
|
using smartbotic::db::storage::LmdbEnv;
|
|
|
@@ -303,6 +305,130 @@ void test_relations_enforced_false_skips_the_check() {
|
|
|
mstore.stop();
|
|
|
}
|
|
|
|
|
|
+// v2.11.0 T5 — DescribeDelete's enumeration. The case that matters most:
|
|
|
+// a parent with children under a restrict relation AND children under a
|
|
|
+// no_action relation. Unlike findRelationBlocks (which only reports
|
|
|
+// blockers), describeDeleteImpacts must report BOTH relations, and mark
|
|
|
+// only the restrict one as blocking.
|
|
|
+void test_describe_delete_reports_restrict_and_no_action() {
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
+ mstore.start();
|
|
|
+ RelationManager rm(mstore);
|
|
|
+ rm.loadFromStore();
|
|
|
+
|
|
|
+ TmpEnv t("rel-describe-mixed");
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
+ store.set_relations("executions", {{"exec_wf", "workflowId"}});
|
|
|
+ store.set_relations("comments", {{"comment_wf", "workflowId"}});
|
|
|
+
|
|
|
+ auto put = [&](const std::string& coll, const std::string& id, const nlohmann::json& data) {
|
|
|
+ Document d; d.id = id; d.collection = coll; d.set_data(data);
|
|
|
+ store.put(coll, id, d);
|
|
|
+ };
|
|
|
+ put("executions", "e1", {{"workflowId", "wf-1"}});
|
|
|
+ put("executions", "e2", {{"workflowId", "wf-1"}});
|
|
|
+ put("comments", "c1", {{"workflowId", "wf-1"}});
|
|
|
+
|
|
|
+ std::string mgrErr;
|
|
|
+ RelationInfo restrictRel;
|
|
|
+ restrictRel.name = "default:exec_wf";
|
|
|
+ restrictRel.child = "default:executions";
|
|
|
+ restrictRel.childField = "workflowId";
|
|
|
+ restrictRel.parent = "default:workflows";
|
|
|
+ restrictRel.onDelete = OnDelete::Restrict;
|
|
|
+ check(rm.createRelation(restrictRel, mgrErr), "declared the restrict relation");
|
|
|
+
|
|
|
+ RelationInfo noActionRel;
|
|
|
+ noActionRel.name = "default:comment_wf";
|
|
|
+ noActionRel.child = "default:comments";
|
|
|
+ noActionRel.childField = "workflowId";
|
|
|
+ noActionRel.parent = "default:workflows";
|
|
|
+ noActionRel.onDelete = OnDelete::NoAction;
|
|
|
+ check(rm.createRelation(noActionRel, mgrErr), "declared the no_action relation");
|
|
|
+
|
|
|
+ auto impacts = describeDeleteImpacts(rm, store, "default:workflows", "wf-1");
|
|
|
+ check(impacts.size() == 2, "both relations are reported, not just the blocker");
|
|
|
+
|
|
|
+ const RelationImpact* restrictImpact = nullptr;
|
|
|
+ const RelationImpact* noActionImpact = nullptr;
|
|
|
+ for (const auto& imp : impacts) {
|
|
|
+ if (imp.relation == "default:exec_wf") restrictImpact = &imp;
|
|
|
+ if (imp.relation == "default:comment_wf") noActionImpact = &imp;
|
|
|
+ }
|
|
|
+ check(restrictImpact != nullptr, "restrict relation present");
|
|
|
+ check(noActionImpact != nullptr, "no_action relation present");
|
|
|
+ if (restrictImpact) {
|
|
|
+ check(restrictImpact->childCount == 2, "restrict relation counts both children");
|
|
|
+ check(restrictImpact->blocks, "restrict relation with live children blocks");
|
|
|
+ check(restrictImpact->sampleChildIds.size() == 2, "samples both blocking ids");
|
|
|
+ }
|
|
|
+ if (noActionImpact) {
|
|
|
+ check(noActionImpact->childCount == 1, "no_action relation counts its child");
|
|
|
+ check(!noActionImpact->blocks, "no_action never blocks, even with live children");
|
|
|
+ check(noActionImpact->sampleChildIds.size() == 1, "still samples the id for visibility");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Consistency with the actual enforcement decision: RelationEnforcer
|
|
|
+ // (what Delete() really calls) agrees that this delete is blocked, by
|
|
|
+ // exactly the relation describeDeleteImpacts flagged.
|
|
|
+ bool enforced = true;
|
|
|
+ RelationEnforcer enforcer(rm, store, enforced);
|
|
|
+ std::string err;
|
|
|
+ check(!enforcer.canDelete("default:workflows", "wf-1", err),
|
|
|
+ "a real delete right now would in fact be blocked, agreeing with the impact");
|
|
|
+
|
|
|
+ mstore.stop();
|
|
|
+}
|
|
|
+
|
|
|
+// A relation with no live children is reported (so an operator can see the
|
|
|
+// declaration exists) but never blocks - zero postings is not a reference,
|
|
|
+// matching findRelationBlocks' "zero count is not a block" rule.
|
|
|
+void test_describe_delete_zero_children_does_not_block() {
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
+ mstore.start();
|
|
|
+ RelationManager rm(mstore);
|
|
|
+ rm.loadFromStore();
|
|
|
+
|
|
|
+ TmpEnv t("rel-describe-empty");
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
+ store.set_relations("executions", {{"exec_wf", "workflowId"}});
|
|
|
+
|
|
|
+ RelationInfo rel;
|
|
|
+ rel.name = "default:exec_wf";
|
|
|
+ rel.child = "default:executions";
|
|
|
+ rel.childField = "workflowId";
|
|
|
+ rel.parent = "default:workflows";
|
|
|
+ rel.onDelete = OnDelete::Restrict;
|
|
|
+ std::string mgrErr;
|
|
|
+ check(rm.createRelation(rel, mgrErr), "declared the relation");
|
|
|
+
|
|
|
+ auto impacts = describeDeleteImpacts(rm, store, "default:workflows", "wf-nonexistent");
|
|
|
+ check(impacts.size() == 1, "the declared relation is reported even with no children");
|
|
|
+ check(impacts[0].childCount == 0, "no children referencing this parent id");
|
|
|
+ check(!impacts[0].blocks, "zero children never blocks");
|
|
|
+ check(impacts[0].sampleChildIds.empty(), "no sample ids when there are no children");
|
|
|
+
|
|
|
+ mstore.stop();
|
|
|
+}
|
|
|
+
|
|
|
+// A collection with no declared relations at all reports an empty impacts
|
|
|
+// list - describeDeleteImpacts, like findRelationBlocks, has nothing to say
|
|
|
+// about a parent nothing points at.
|
|
|
+void test_describe_delete_no_relations_declared() {
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
+ mstore.start();
|
|
|
+ RelationManager rm(mstore);
|
|
|
+ rm.loadFromStore();
|
|
|
+
|
|
|
+ TmpEnv t("rel-describe-none");
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
+
|
|
|
+ auto impacts = describeDeleteImpacts(rm, store, "default:orphan_collection", "anything");
|
|
|
+ check(impacts.empty(), "no relations declared means no impacts reported");
|
|
|
+
|
|
|
+ mstore.stop();
|
|
|
+}
|
|
|
+
|
|
|
} // namespace
|
|
|
|
|
|
int main() {
|
|
|
@@ -314,6 +440,9 @@ int main() {
|
|
|
test_restrict_blocks_and_names_the_blockers();
|
|
|
test_cascade_and_set_null_permit_for_now();
|
|
|
test_relations_enforced_false_skips_the_check();
|
|
|
+ test_describe_delete_reports_restrict_and_no_action();
|
|
|
+ test_describe_delete_zero_children_does_not_block();
|
|
|
+ test_describe_delete_no_relations_declared();
|
|
|
|
|
|
std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
|
|
|
return g_fail == 0 ? 0 : 1;
|