test_project_crud.cpp 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // v2.3 Stage F — project CRUD placeholder tests.
  2. //
  3. // The placeholder is filesystem-only (under <dataDir>/projects/) and the
  4. // helpers live in `service/src/project_fs_placeholder.hpp` as header-only
  5. // inline functions. DatabaseService::{listProjects, createProject,
  6. // dropProject} are thin wrappers that take a mutex and delegate. Testing
  7. // the helpers directly exercises the same code path without dragging in
  8. // the full DatabaseService link surface (memory_store, persistence,
  9. // encryption, grpc, etc.) — same approach test_project_addressing takes
  10. // for the parser. Stage B replaces this whole helper with the real
  11. // ProjectStore-backed implementation.
  12. #include "project_fs_placeholder.hpp"
  13. #include <algorithm>
  14. #include <cstdlib>
  15. #include <filesystem>
  16. #include <iostream>
  17. #include <string>
  18. #include <unistd.h> // getpid
  19. namespace fs = std::filesystem;
  20. namespace placeholder = smartbotic::database::project_fs_placeholder;
  21. using smartbotic::database::kDefaultProject;
  22. namespace {
  23. int g_pass = 0;
  24. int g_fail = 0;
  25. void check(bool cond, const char* msg) {
  26. if (cond) ++g_pass;
  27. else { ++g_fail; std::cerr << "FAIL: " << msg << "\n"; }
  28. }
  29. fs::path makeTmpDataDir(const std::string& tag) {
  30. auto dir = fs::temp_directory_path() /
  31. ("project-crud-" + tag + "-" + std::to_string(::getpid()));
  32. fs::remove_all(dir);
  33. fs::create_directories(dir);
  34. return dir;
  35. }
  36. bool contains(const std::vector<std::string>& v, const std::string& s) {
  37. return std::find(v.begin(), v.end(), s) != v.end();
  38. }
  39. // ---- Tests ----
  40. // listProjects() on a fresh dataDir returns just {"default"} (the default
  41. // project is implicit until the first write).
  42. void test_list_on_fresh_data_dir() {
  43. auto dir = makeTmpDataDir("list-fresh");
  44. auto names = placeholder::listProjects(dir);
  45. check(names.size() == 1, "fresh dataDir: exactly one project");
  46. check(!names.empty() && names[0] == kDefaultProject,
  47. "fresh dataDir: that project is 'default'");
  48. fs::remove_all(dir);
  49. }
  50. // createProject("foo") returns true the first time, then listProjects
  51. // includes "foo".
  52. void test_create_then_list_includes() {
  53. auto dir = makeTmpDataDir("create-list");
  54. std::string error;
  55. bool created = placeholder::createProject(dir, "foo", error);
  56. check(created, "createProject('foo') returns true on first call");
  57. check(error.empty(), "createProject('foo') sets no error on success");
  58. auto names = placeholder::listProjects(dir);
  59. check(contains(names, "foo"), "listProjects includes 'foo' after create");
  60. check(contains(names, kDefaultProject),
  61. "listProjects still includes 'default' after create");
  62. check(std::is_sorted(names.begin(), names.end()),
  63. "listProjects returns sorted list");
  64. fs::remove_all(dir);
  65. }
  66. // createProject is idempotent: second call returns false, no error.
  67. void test_create_idempotent() {
  68. auto dir = makeTmpDataDir("create-idempotent");
  69. std::string error;
  70. bool first = placeholder::createProject(dir, "foo", error);
  71. check(first, "first createProject('foo') returns true");
  72. error = "stale";
  73. bool second = placeholder::createProject(dir, "foo", error);
  74. check(!second, "second createProject('foo') returns false (idempotent)");
  75. check(error.empty(),
  76. "second createProject('foo') leaves error EMPTY (no failure)");
  77. fs::remove_all(dir);
  78. }
  79. // createProject("_bad") — leading underscore is reserved.
  80. void test_create_rejects_underscore_prefix() {
  81. auto dir = makeTmpDataDir("create-underscore");
  82. std::string error;
  83. bool created = placeholder::createProject(dir, "_bad", error);
  84. check(!created, "createProject('_bad') returns false");
  85. check(!error.empty(), "createProject('_bad') sets a non-empty error");
  86. // Sanity: the error message should mention the name so operators
  87. // know which input was rejected.
  88. check(error.find("_bad") != std::string::npos,
  89. "createProject('_bad') error mentions the offending name");
  90. fs::remove_all(dir);
  91. }
  92. // createProject("9bad") — leading digit is invalid.
  93. void test_create_rejects_digit_prefix() {
  94. auto dir = makeTmpDataDir("create-digit");
  95. std::string error;
  96. bool created = placeholder::createProject(dir, "9bad", error);
  97. check(!created, "createProject('9bad') returns false");
  98. check(!error.empty(), "createProject('9bad') sets a non-empty error");
  99. check(error.find("9bad") != std::string::npos,
  100. "createProject('9bad') error mentions the offending name");
  101. fs::remove_all(dir);
  102. }
  103. // dropProject("default") is refused — operator must never lose the
  104. // default namespace.
  105. void test_drop_default_refused() {
  106. auto dir = makeTmpDataDir("drop-default");
  107. std::string error;
  108. bool dropped = placeholder::dropProject(dir, kDefaultProject, error);
  109. check(!dropped, "dropProject('default') returns false");
  110. check(!error.empty(), "dropProject('default') sets a non-empty error");
  111. check(error.find("default") != std::string::npos,
  112. "dropProject('default') error mentions 'default'");
  113. fs::remove_all(dir);
  114. }
  115. // dropProject of a real project succeeds; subsequent listProjects no
  116. // longer includes it.
  117. void test_drop_then_list_excludes() {
  118. auto dir = makeTmpDataDir("drop-then-list");
  119. std::string error;
  120. bool created = placeholder::createProject(dir, "foo", error);
  121. check(created, "setup: createProject('foo') returns true");
  122. error = "stale";
  123. bool dropped = placeholder::dropProject(dir, "foo", error);
  124. check(dropped, "dropProject('foo') returns true");
  125. check(error.empty(), "dropProject('foo') sets no error on success");
  126. auto names = placeholder::listProjects(dir);
  127. check(!contains(names, "foo"),
  128. "listProjects no longer includes 'foo' after drop");
  129. check(names.size() == 1 && names[0] == kDefaultProject,
  130. "listProjects returns just ['default'] after drop");
  131. fs::remove_all(dir);
  132. }
  133. // dropProject of an unknown project returns false with error.
  134. void test_drop_nonexistent_refused() {
  135. auto dir = makeTmpDataDir("drop-nonexistent");
  136. std::string error;
  137. bool dropped = placeholder::dropProject(dir, "nonexistent", error);
  138. check(!dropped, "dropProject('nonexistent') returns false");
  139. check(!error.empty(),
  140. "dropProject('nonexistent') sets a non-empty error");
  141. check(error.find("nonexistent") != std::string::npos ||
  142. error.find("not found") != std::string::npos,
  143. "dropProject('nonexistent') error indicates the missing name");
  144. fs::remove_all(dir);
  145. }
  146. // Additional sanity — directory structure matches the documented layout:
  147. // <dataDir>/projects/<name>/env/
  148. void test_create_writes_documented_layout() {
  149. auto dir = makeTmpDataDir("layout-check");
  150. std::string error;
  151. bool created = placeholder::createProject(dir, "alpha", error);
  152. check(created, "setup: createProject('alpha') returns true");
  153. check(fs::is_directory(dir / "projects" / "alpha" / "env"),
  154. "<dataDir>/projects/alpha/env/ exists after createProject");
  155. fs::remove_all(dir);
  156. }
  157. } // namespace
  158. int main() {
  159. test_list_on_fresh_data_dir();
  160. test_create_then_list_includes();
  161. test_create_idempotent();
  162. test_create_rejects_underscore_prefix();
  163. test_create_rejects_digit_prefix();
  164. test_drop_default_refused();
  165. test_drop_then_list_excludes();
  166. test_drop_nonexistent_refused();
  167. test_create_writes_documented_layout();
  168. std::cout << "project_crud: " << g_pass << " passed, "
  169. << g_fail << " failed\n";
  170. return g_fail == 0 ? 0 : 1;
  171. }