test_relation_manager.cpp 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // Task 1 — RelationManager: the declaration registry for referential
  2. // integrity relations. No enforcement, no LMDB index yet (later tasks).
  3. //
  4. // Modeled on tests/test_view_manager_paging.cpp: relations are keyed by
  5. // their project-qualified name in a `_relations` system collection, and
  6. // loadFromStore() must page explicitly since Query::limit defaults to 100
  7. // and limit=0 returns nothing (not everything) — the same trap that has
  8. // already shipped as a bug in ViewManager, PolicyManager and
  9. // CollectionConfigManager.
  10. #include <iostream>
  11. #include <string>
  12. #include <nlohmann/json.hpp>
  13. #include "memory_store.hpp"
  14. #include "relations/relation_manager.hpp"
  15. using namespace smartbotic::database;
  16. namespace {
  17. int g_pass = 0;
  18. int g_fail = 0;
  19. void check(bool cond, const std::string& msg) {
  20. if (cond) { ++g_pass; }
  21. else { ++g_fail; std::cerr << "FAIL: " << msg << "\n"; }
  22. }
  23. struct Fixture {
  24. MemoryStore store;
  25. Fixture() : store(MemoryStore::Config{}) {
  26. store.start();
  27. }
  28. ~Fixture() { store.stop(); }
  29. };
  30. void test_relations_are_project_scoped_and_survive_reload() {
  31. Fixture f; // MemoryStore, started
  32. RelationManager rm(f.store);
  33. rm.loadFromStore();
  34. RelationInfo a;
  35. a.name = "default:exec_wf";
  36. a.child = "default:executions";
  37. a.childField = "workflowId";
  38. a.parent = "default:workflows";
  39. std::string err;
  40. check(rm.createRelation(a, err), "created in default");
  41. RelationInfo b = a; // SAME bare name, different project
  42. b.name = "acme:exec_wf";
  43. b.child = "acme:executions";
  44. b.parent = "acme:workflows";
  45. check(rm.createRelation(b, err), "the same name in another project is allowed");
  46. check(rm.listRelations("default").size() == 1, "listing is project-filtered");
  47. check(rm.listRelations().size() == 2, "empty project lists everything");
  48. RelationManager fresh(f.store); // restart
  49. fresh.loadFromStore();
  50. check(fresh.getRelation("default:exec_wf").has_value(), "survives reload");
  51. check(fresh.getRelation("acme:exec_wf").has_value(), "both survive");
  52. }
  53. void test_cross_project_relation_is_refused() {
  54. Fixture f;
  55. RelationManager rm(f.store);
  56. RelationInfo r;
  57. r.name = "default:bad";
  58. r.child = "default:executions";
  59. r.childField = "workflowId";
  60. r.parent = "acme:workflows"; // different env - no txn spans two
  61. std::string err;
  62. check(!rm.createRelation(r, err), "a cross-project relation is refused");
  63. check(err.find("project") != std::string::npos, "and says why");
  64. }
  65. void test_more_than_one_page_of_relations_loads() {
  66. Fixture f;
  67. RelationManager rm(f.store);
  68. for (int i = 0; i < 250; ++i) { // Query::limit defaults to 100
  69. RelationInfo r;
  70. char buf[32];
  71. std::snprintf(buf, sizeof(buf), "default:r%03d", i);
  72. r.name = buf;
  73. r.child = "default:c";
  74. r.childField = "p";
  75. r.parent = "default:p";
  76. std::string err;
  77. rm.createRelation(r, err);
  78. }
  79. RelationManager fresh(f.store);
  80. fresh.loadFromStore();
  81. check(fresh.listRelations().size() == 250,
  82. "all 250 load - a bare Query would stop at 100, as it did for views, "
  83. "policies and collection configs");
  84. }
  85. } // namespace
  86. int main() {
  87. std::cout << "=== test_relation_manager ===\n";
  88. test_relations_are_project_scoped_and_survive_reload();
  89. test_cross_project_relation_is_refused();
  90. test_more_than_one_page_of_relations_loads();
  91. std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
  92. return g_fail == 0 ? 0 : 1;
  93. }