|
@@ -4,6 +4,12 @@
|
|
|
// reverse index declared via set_relations(), using the same set-difference
|
|
// reverse index declared via set_relations(), using the same set-difference
|
|
|
// discipline as maintainIndexes(). No enforcement (Task 4) is involved here
|
|
// discipline as maintainIndexes(). No enforcement (Task 4) is involved here
|
|
|
// - relation_index_child_count is used purely as the observation point.
|
|
// - relation_index_child_count is used purely as the observation point.
|
|
|
|
|
+//
|
|
|
|
|
+// v2.11.0 T4 adds restrict/no_action delete enforcement below
|
|
|
|
|
+// (test_restrict_blocks_and_names_the_blockers,
|
|
|
|
|
+// test_relations_enforced_false_skips_the_check) - unit-level, against
|
|
|
|
|
+// RelationManager + RelationEnforcer + LmdbDocumentStore directly, no
|
|
|
|
|
+// grpc::ServerContext. The RPC-level check lands in Task 6/9.
|
|
|
|
|
|
|
|
#include <atomic>
|
|
#include <atomic>
|
|
|
#include <cstdio>
|
|
#include <cstdio>
|
|
@@ -16,12 +22,22 @@
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
|
|
|
#include "document.hpp"
|
|
#include "document.hpp"
|
|
|
|
|
+#include "memory_store.hpp"
|
|
|
|
|
+#include "relations/relation_enforcement.hpp"
|
|
|
|
|
+#include "relations/relation_manager.hpp"
|
|
|
#include "storage/document_store_lmdb.hpp"
|
|
#include "storage/document_store_lmdb.hpp"
|
|
|
#include "storage/lmdb_env.hpp"
|
|
#include "storage/lmdb_env.hpp"
|
|
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
using smartbotic::database::Document;
|
|
using smartbotic::database::Document;
|
|
|
|
|
+using smartbotic::database::MemoryStore;
|
|
|
|
|
+using smartbotic::database::OnDelete;
|
|
|
|
|
+using smartbotic::database::RelationBlock;
|
|
|
|
|
+using smartbotic::database::RelationEnforcer;
|
|
|
|
|
+using smartbotic::database::RelationInfo;
|
|
|
|
|
+using smartbotic::database::RelationManager;
|
|
|
|
|
+using smartbotic::database::findRelationBlocks;
|
|
|
using smartbotic::db::storage::LmdbDocumentStore;
|
|
using smartbotic::db::storage::LmdbDocumentStore;
|
|
|
using smartbotic::db::storage::LmdbEnv;
|
|
using smartbotic::db::storage::LmdbEnv;
|
|
|
using smartbotic::db::storage::LmdbEnvOpts;
|
|
using smartbotic::db::storage::LmdbEnvOpts;
|
|
@@ -150,6 +166,143 @@ void test_posting_visible_after_reopen() {
|
|
|
"posting survives a fresh LmdbDocumentStore over the same env");
|
|
"posting survives a fresh LmdbDocumentStore over the same env");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// v2.11.0 T4 — restrict blocks a delete and names the blockers; no_action
|
|
|
|
|
+// permits it and leaves the reference dangling, deliberately.
|
|
|
|
|
+//
|
|
|
|
|
+// RelationManager's declaration registry lives in a MemoryStore's
|
|
|
|
|
+// `_relations` collection (see test_relation_manager.cpp's Fixture); the
|
|
|
|
|
+// reverse index it queries lives in a separate per-project LmdbDocumentStore
|
|
|
|
|
+// (T2/T3, exercised above). This test wires both together the way Task 6's
|
|
|
|
|
+// Delete handler eventually will.
|
|
|
|
|
+void test_restrict_blocks_and_names_the_blockers() {
|
|
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
|
|
+ mstore.start();
|
|
|
|
|
+ RelationManager rm(mstore);
|
|
|
|
|
+ rm.loadFromStore();
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv t("rel-enforce-restrict");
|
|
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
|
|
+ store.set_relations("executions", {{"exec_wf", "workflowId"}});
|
|
|
|
|
+
|
|
|
|
|
+ auto put = [&](const std::string& id, const nlohmann::json& data) {
|
|
|
|
|
+ Document d; d.id = id; d.collection = "executions"; d.set_data(data);
|
|
|
|
|
+ store.put("executions", id, d);
|
|
|
|
|
+ };
|
|
|
|
|
+ put("e1", {{"workflowId", "wf-1"}});
|
|
|
|
|
+ put("e2", {{"workflowId", "wf-1"}});
|
|
|
|
|
+
|
|
|
|
|
+ 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 restrict relation");
|
|
|
|
|
+
|
|
|
|
|
+ bool enforced = true;
|
|
|
|
|
+ RelationEnforcer enforcer(rm, store, enforced);
|
|
|
|
|
+
|
|
|
|
|
+ std::string err;
|
|
|
|
|
+ check(!enforcer.canDelete("default:workflows", "wf-1", err), "restrict blocks");
|
|
|
|
|
+ check(err.find("exec_wf") != std::string::npos, "names the relation");
|
|
|
|
|
+ check(err.find("2") != std::string::npos, "gives the child count");
|
|
|
|
|
+ check(err.find("e1") != std::string::npos, "samples a blocking id");
|
|
|
|
|
+
|
|
|
|
|
+ // no_action permits it and leaves the reference dangling, deliberately.
|
|
|
|
|
+ // RelationManager has no update-in-place; re-declare with the new
|
|
|
|
|
+ // policy the same way an operator would via dropRelation + createRelation.
|
|
|
|
|
+ check(rm.dropRelation(rel.name, mgrErr), "dropped to change on_delete");
|
|
|
|
|
+ rel.onDelete = OnDelete::NoAction;
|
|
|
|
|
+ check(rm.createRelation(rel, mgrErr), "re-declared as no_action");
|
|
|
|
|
+
|
|
|
|
|
+ err.clear();
|
|
|
|
|
+ check(enforcer.canDelete("default:workflows", "wf-1", err), "no_action permits");
|
|
|
|
|
+
|
|
|
|
|
+ mstore.stop();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Cascade/set_null are NOT yet destructive (Task 12) - a relation declaring
|
|
|
|
|
+// either one must behave exactly like no_action here, not block, and not
|
|
|
|
|
+// half-implement destruction.
|
|
|
|
|
+void test_cascade_and_set_null_permit_for_now() {
|
|
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
|
|
+ mstore.start();
|
|
|
|
|
+ RelationManager rm(mstore);
|
|
|
|
|
+ rm.loadFromStore();
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv t("rel-enforce-cascade");
|
|
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
|
|
+ store.set_relations("executions", {{"exec_wf", "workflowId"}});
|
|
|
|
|
+
|
|
|
|
|
+ Document d; d.id = "e1"; d.collection = "executions";
|
|
|
|
|
+ d.set_data({{"workflowId", "wf-1"}});
|
|
|
|
|
+ store.put("executions", "e1", d);
|
|
|
|
|
+
|
|
|
|
|
+ RelationInfo rel;
|
|
|
|
|
+ rel.name = "default:exec_wf";
|
|
|
|
|
+ rel.child = "default:executions";
|
|
|
|
|
+ rel.childField = "workflowId";
|
|
|
|
|
+ rel.parent = "default:workflows";
|
|
|
|
|
+ rel.onDelete = OnDelete::Cascade;
|
|
|
|
|
+
|
|
|
|
|
+ std::string mgrErr;
|
|
|
|
|
+ check(rm.createRelation(rel, mgrErr), "declared a cascade relation");
|
|
|
|
|
+
|
|
|
|
|
+ auto blocks = findRelationBlocks(rm, store, "default:workflows", "wf-1");
|
|
|
|
|
+ check(blocks.empty(), "cascade does not block - not yet destructive (Task 12)");
|
|
|
|
|
+
|
|
|
|
|
+ check(rm.dropRelation(rel.name, mgrErr), "dropped to change on_delete");
|
|
|
|
|
+ rel.onDelete = OnDelete::SetNull;
|
|
|
|
|
+ check(rm.createRelation(rel, mgrErr), "re-declared as set_null");
|
|
|
|
|
+
|
|
|
|
|
+ blocks = findRelationBlocks(rm, store, "default:workflows", "wf-1");
|
|
|
|
|
+ check(blocks.empty(), "set_null does not block - not yet destructive (Task 12)");
|
|
|
|
|
+
|
|
|
|
|
+ mstore.stop();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// relationsEnforced=false (CollectionCfg, Task 8) skips the check entirely,
|
|
|
|
|
+// even though a restrict relation with live children exists. Deliberately
|
|
|
|
|
+// not retroactive - see RelationEnforcer::canDelete's doc comment.
|
|
|
|
|
+void test_relations_enforced_false_skips_the_check() {
|
|
|
|
|
+ MemoryStore mstore(MemoryStore::Config{});
|
|
|
|
|
+ mstore.start();
|
|
|
|
|
+ RelationManager rm(mstore);
|
|
|
|
|
+ rm.loadFromStore();
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv t("rel-enforce-disabled");
|
|
|
|
|
+ LmdbDocumentStore store(t.env);
|
|
|
|
|
+ store.set_relations("executions", {{"exec_wf", "workflowId"}});
|
|
|
|
|
+
|
|
|
|
|
+ Document d; d.id = "e1"; d.collection = "executions";
|
|
|
|
|
+ d.set_data({{"workflowId", "wf-1"}});
|
|
|
|
|
+ store.put("executions", "e1", d);
|
|
|
|
|
+
|
|
|
|
|
+ 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 restrict relation");
|
|
|
|
|
+
|
|
|
|
|
+ bool enforced = false; // per-collection switch, Task 8
|
|
|
|
|
+ RelationEnforcer enforcer(rm, store, enforced);
|
|
|
|
|
+
|
|
|
|
|
+ std::string err;
|
|
|
|
|
+ check(enforcer.canDelete("default:workflows", "wf-1", err),
|
|
|
|
|
+ "enforcement disabled for this collection lets the delete through - and "
|
|
|
|
|
+ "the resulting dangling references will NOT be found by re-enabling it, "
|
|
|
|
|
+ "only by `relations check`");
|
|
|
|
|
+ check(err.empty(), "no error text is populated on a permitted delete");
|
|
|
|
|
+
|
|
|
|
|
+ mstore.stop();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
} // namespace
|
|
} // namespace
|
|
|
|
|
|
|
|
int main() {
|
|
int main() {
|
|
@@ -158,6 +311,9 @@ int main() {
|
|
|
test_undeclared_collection_maintains_nothing();
|
|
test_undeclared_collection_maintains_nothing();
|
|
|
test_unrelated_update_leaves_the_posting_alone();
|
|
test_unrelated_update_leaves_the_posting_alone();
|
|
|
test_posting_visible_after_reopen();
|
|
test_posting_visible_after_reopen();
|
|
|
|
|
+ test_restrict_blocks_and_names_the_blockers();
|
|
|
|
|
+ test_cascade_and_set_null_permit_for_now();
|
|
|
|
|
+ test_relations_enforced_false_skips_the_check();
|
|
|
|
|
|
|
|
std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
|
|
std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
|
|
|
return g_fail == 0 ? 0 : 1;
|
|
return g_fail == 0 ? 0 : 1;
|