| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- // Task 1 — RelationManager: the declaration registry for referential
- // integrity relations. No enforcement, no LMDB index yet (later tasks).
- //
- // Modeled on tests/test_view_manager_paging.cpp: relations are keyed by
- // their project-qualified name in a `_relations` system collection, and
- // loadFromStore() must page explicitly since Query::limit defaults to 100
- // and limit=0 returns nothing (not everything) — the same trap that has
- // already shipped as a bug in ViewManager, PolicyManager and
- // CollectionConfigManager.
- #include <iostream>
- #include <string>
- #include <nlohmann/json.hpp>
- #include "document.hpp"
- #include "memory_store.hpp"
- #include "relations/relation_manager.hpp"
- using namespace smartbotic::database;
- namespace {
- int g_pass = 0;
- int g_fail = 0;
- void check(bool cond, const std::string& msg) {
- if (cond) { ++g_pass; }
- else { ++g_fail; std::cerr << "FAIL: " << msg << "\n"; }
- }
- struct Fixture {
- MemoryStore store;
- Fixture() : store(MemoryStore::Config{}) {
- store.start();
- }
- ~Fixture() { store.stop(); }
- };
- void test_relations_are_project_scoped_and_survive_reload() {
- Fixture f; // MemoryStore, started
- RelationManager rm(f.store);
- rm.loadFromStore();
- RelationInfo a;
- a.name = "default:exec_wf";
- a.child = "default:executions";
- a.childField = "workflowId";
- a.parent = "default:workflows";
- std::string err;
- check(rm.createRelation(a, err), "created in default");
- RelationInfo b = a; // SAME bare name, different project
- b.name = "acme:exec_wf";
- b.child = "acme:executions";
- b.parent = "acme:workflows";
- check(rm.createRelation(b, err), "the same name in another project is allowed");
- check(rm.listRelations("default").size() == 1, "listing is project-filtered");
- check(rm.listRelations().size() == 2, "empty project lists everything");
- RelationManager fresh(f.store); // restart
- fresh.loadFromStore();
- check(fresh.getRelation("default:exec_wf").has_value(), "survives reload");
- check(fresh.getRelation("acme:exec_wf").has_value(), "both survive");
- }
- void test_cross_project_relation_is_refused() {
- Fixture f;
- RelationManager rm(f.store);
- RelationInfo r;
- r.name = "default:bad";
- r.child = "default:executions";
- r.childField = "workflowId";
- r.parent = "acme:workflows"; // different env - no txn spans two
- std::string err;
- check(!rm.createRelation(r, err), "a cross-project relation is refused");
- check(err.find("project") != std::string::npos, "and says why");
- }
- void test_more_than_one_page_of_relations_loads() {
- Fixture f;
- RelationManager rm(f.store);
- for (int i = 0; i < 250; ++i) { // Query::limit defaults to 100
- RelationInfo r;
- char buf[32];
- std::snprintf(buf, sizeof(buf), "default:r%03d", i);
- r.name = buf;
- r.child = "default:c";
- r.childField = "p";
- r.parent = "default:p";
- std::string err;
- rm.createRelation(r, err);
- }
- RelationManager fresh(f.store);
- fresh.loadFromStore();
- check(fresh.listRelations().size() == 250,
- "all 250 load - a bare Query would stop at 100, as it did for views, "
- "policies and collection configs");
- }
- // v2.11.0 T13 round 2 (review finding 3) — createRelation() refuses a
- // cross-project declaration (test_cross_project_relation_is_refused,
- // above), but loadFromStore() is the OTHER way a RelationInfo enters the
- // cache and did not re-check it. A legacy or hand-written `_relations`
- // document naming a cross-project parent must be skipped at load time too -
- // arming it would resolve the bare parent name inside the CHILD's own
- // project env (validate_on_write/RelationRef only ever resolve `parent`
- // against the child's project), silently checking the wrong collection.
- void test_load_skips_a_cross_project_relation_written_by_hand() {
- Fixture f;
- // Bypass createRelation()'s own guard entirely - write the raw document
- // straight into `_relations`, the way a legacy record or a hand-edited
- // one would exist on disk. `parent` names a DIFFERENT project than
- // `child`, which createRelation() would refuse today.
- nlohmann::json bad = {
- {"name", "default:bad_cross"},
- {"child", "default:executions"},
- {"child_field", "workflowId"},
- {"parent", "acme:workflows"},
- {"on_delete", "restrict"},
- {"validate_on_write", true},
- {"created_at", 0},
- {"updated_at", 0},
- };
- Document d;
- d.id = "default:bad_cross";
- d.collection = RelationManager::SYSTEM_COLLECTION;
- d.set_data(bad);
- f.store.insert(RelationManager::SYSTEM_COLLECTION, d);
- // A well-formed, same-project relation alongside it, to confirm one bad
- // record does not stop the rest of the load.
- RelationInfo good;
- good.name = "default:exec_wf";
- good.child = "default:executions";
- good.childField = "ownerId";
- good.parent = "default:users";
- {
- RelationManager rm(f.store);
- rm.loadFromStore();
- std::string err;
- check(rm.createRelation(good, err), "the well-formed sibling declares fine");
- }
- RelationManager fresh(f.store);
- fresh.loadFromStore();
- check(!fresh.getRelation("default:bad_cross").has_value(),
- "the cross-project relation was skipped, not armed with the wrong parent");
- check(fresh.getRelation("default:exec_wf").has_value(),
- "the well-formed sibling still loaded - one bad record did not stop the rest");
- }
- } // namespace
- int main() {
- std::cout << "=== test_relation_manager ===\n";
- test_relations_are_project_scoped_and_survive_reload();
- test_cross_project_relation_is_refused();
- test_more_than_one_page_of_relations_loads();
- test_load_skips_a_cross_project_relation_written_by_hand();
- std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
- return g_fail == 0 ? 0 : 1;
- }
|