|
|
@@ -0,0 +1,196 @@
|
|
|
+// v2.3 Stage F — project CRUD placeholder tests.
|
|
|
+//
|
|
|
+// The placeholder is filesystem-only (under <dataDir>/projects/) and the
|
|
|
+// helpers live in `service/src/project_fs_placeholder.hpp` as header-only
|
|
|
+// inline functions. DatabaseService::{listProjects, createProject,
|
|
|
+// dropProject} are thin wrappers that take a mutex and delegate. Testing
|
|
|
+// the helpers directly exercises the same code path without dragging in
|
|
|
+// the full DatabaseService link surface (memory_store, persistence,
|
|
|
+// encryption, grpc, etc.) — same approach test_project_addressing takes
|
|
|
+// for the parser. Stage B replaces this whole helper with the real
|
|
|
+// ProjectStore-backed implementation.
|
|
|
+
|
|
|
+#include "project_fs_placeholder.hpp"
|
|
|
+
|
|
|
+#include <algorithm>
|
|
|
+#include <cstdlib>
|
|
|
+#include <filesystem>
|
|
|
+#include <iostream>
|
|
|
+#include <string>
|
|
|
+#include <unistd.h> // getpid
|
|
|
+
|
|
|
+namespace fs = std::filesystem;
|
|
|
+namespace placeholder = smartbotic::database::project_fs_placeholder;
|
|
|
+using smartbotic::database::kDefaultProject;
|
|
|
+
|
|
|
+namespace {
|
|
|
+
|
|
|
+int g_pass = 0;
|
|
|
+int g_fail = 0;
|
|
|
+
|
|
|
+void check(bool cond, const char* msg) {
|
|
|
+ if (cond) ++g_pass;
|
|
|
+ else { ++g_fail; std::cerr << "FAIL: " << msg << "\n"; }
|
|
|
+}
|
|
|
+
|
|
|
+fs::path makeTmpDataDir(const std::string& tag) {
|
|
|
+ auto dir = fs::temp_directory_path() /
|
|
|
+ ("project-crud-" + tag + "-" + std::to_string(::getpid()));
|
|
|
+ fs::remove_all(dir);
|
|
|
+ fs::create_directories(dir);
|
|
|
+ return dir;
|
|
|
+}
|
|
|
+
|
|
|
+bool contains(const std::vector<std::string>& v, const std::string& s) {
|
|
|
+ return std::find(v.begin(), v.end(), s) != v.end();
|
|
|
+}
|
|
|
+
|
|
|
+// ---- Tests ----
|
|
|
+
|
|
|
+// listProjects() on a fresh dataDir returns just {"default"} (the default
|
|
|
+// project is implicit until the first write).
|
|
|
+void test_list_on_fresh_data_dir() {
|
|
|
+ auto dir = makeTmpDataDir("list-fresh");
|
|
|
+ auto names = placeholder::listProjects(dir);
|
|
|
+ check(names.size() == 1, "fresh dataDir: exactly one project");
|
|
|
+ check(!names.empty() && names[0] == kDefaultProject,
|
|
|
+ "fresh dataDir: that project is 'default'");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// createProject("foo") returns true the first time, then listProjects
|
|
|
+// includes "foo".
|
|
|
+void test_create_then_list_includes() {
|
|
|
+ auto dir = makeTmpDataDir("create-list");
|
|
|
+ std::string error;
|
|
|
+ bool created = placeholder::createProject(dir, "foo", error);
|
|
|
+ check(created, "createProject('foo') returns true on first call");
|
|
|
+ check(error.empty(), "createProject('foo') sets no error on success");
|
|
|
+
|
|
|
+ auto names = placeholder::listProjects(dir);
|
|
|
+ check(contains(names, "foo"), "listProjects includes 'foo' after create");
|
|
|
+ check(contains(names, kDefaultProject),
|
|
|
+ "listProjects still includes 'default' after create");
|
|
|
+ check(std::is_sorted(names.begin(), names.end()),
|
|
|
+ "listProjects returns sorted list");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// createProject is idempotent: second call returns false, no error.
|
|
|
+void test_create_idempotent() {
|
|
|
+ auto dir = makeTmpDataDir("create-idempotent");
|
|
|
+ std::string error;
|
|
|
+ bool first = placeholder::createProject(dir, "foo", error);
|
|
|
+ check(first, "first createProject('foo') returns true");
|
|
|
+
|
|
|
+ error = "stale";
|
|
|
+ bool second = placeholder::createProject(dir, "foo", error);
|
|
|
+ check(!second, "second createProject('foo') returns false (idempotent)");
|
|
|
+ check(error.empty(),
|
|
|
+ "second createProject('foo') leaves error EMPTY (no failure)");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// createProject("_bad") — leading underscore is reserved.
|
|
|
+void test_create_rejects_underscore_prefix() {
|
|
|
+ auto dir = makeTmpDataDir("create-underscore");
|
|
|
+ std::string error;
|
|
|
+ bool created = placeholder::createProject(dir, "_bad", error);
|
|
|
+ check(!created, "createProject('_bad') returns false");
|
|
|
+ check(!error.empty(), "createProject('_bad') sets a non-empty error");
|
|
|
+ // Sanity: the error message should mention the name so operators
|
|
|
+ // know which input was rejected.
|
|
|
+ check(error.find("_bad") != std::string::npos,
|
|
|
+ "createProject('_bad') error mentions the offending name");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// createProject("9bad") — leading digit is invalid.
|
|
|
+void test_create_rejects_digit_prefix() {
|
|
|
+ auto dir = makeTmpDataDir("create-digit");
|
|
|
+ std::string error;
|
|
|
+ bool created = placeholder::createProject(dir, "9bad", error);
|
|
|
+ check(!created, "createProject('9bad') returns false");
|
|
|
+ check(!error.empty(), "createProject('9bad') sets a non-empty error");
|
|
|
+ check(error.find("9bad") != std::string::npos,
|
|
|
+ "createProject('9bad') error mentions the offending name");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// dropProject("default") is refused — operator must never lose the
|
|
|
+// default namespace.
|
|
|
+void test_drop_default_refused() {
|
|
|
+ auto dir = makeTmpDataDir("drop-default");
|
|
|
+ std::string error;
|
|
|
+ bool dropped = placeholder::dropProject(dir, kDefaultProject, error);
|
|
|
+ check(!dropped, "dropProject('default') returns false");
|
|
|
+ check(!error.empty(), "dropProject('default') sets a non-empty error");
|
|
|
+ check(error.find("default") != std::string::npos,
|
|
|
+ "dropProject('default') error mentions 'default'");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// dropProject of a real project succeeds; subsequent listProjects no
|
|
|
+// longer includes it.
|
|
|
+void test_drop_then_list_excludes() {
|
|
|
+ auto dir = makeTmpDataDir("drop-then-list");
|
|
|
+ std::string error;
|
|
|
+ bool created = placeholder::createProject(dir, "foo", error);
|
|
|
+ check(created, "setup: createProject('foo') returns true");
|
|
|
+
|
|
|
+ error = "stale";
|
|
|
+ bool dropped = placeholder::dropProject(dir, "foo", error);
|
|
|
+ check(dropped, "dropProject('foo') returns true");
|
|
|
+ check(error.empty(), "dropProject('foo') sets no error on success");
|
|
|
+
|
|
|
+ auto names = placeholder::listProjects(dir);
|
|
|
+ check(!contains(names, "foo"),
|
|
|
+ "listProjects no longer includes 'foo' after drop");
|
|
|
+ check(names.size() == 1 && names[0] == kDefaultProject,
|
|
|
+ "listProjects returns just ['default'] after drop");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// dropProject of an unknown project returns false with error.
|
|
|
+void test_drop_nonexistent_refused() {
|
|
|
+ auto dir = makeTmpDataDir("drop-nonexistent");
|
|
|
+ std::string error;
|
|
|
+ bool dropped = placeholder::dropProject(dir, "nonexistent", error);
|
|
|
+ check(!dropped, "dropProject('nonexistent') returns false");
|
|
|
+ check(!error.empty(),
|
|
|
+ "dropProject('nonexistent') sets a non-empty error");
|
|
|
+ check(error.find("nonexistent") != std::string::npos ||
|
|
|
+ error.find("not found") != std::string::npos,
|
|
|
+ "dropProject('nonexistent') error indicates the missing name");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+// Additional sanity — directory structure matches the documented layout:
|
|
|
+// <dataDir>/projects/<name>/env/
|
|
|
+void test_create_writes_documented_layout() {
|
|
|
+ auto dir = makeTmpDataDir("layout-check");
|
|
|
+ std::string error;
|
|
|
+ bool created = placeholder::createProject(dir, "alpha", error);
|
|
|
+ check(created, "setup: createProject('alpha') returns true");
|
|
|
+ check(fs::is_directory(dir / "projects" / "alpha" / "env"),
|
|
|
+ "<dataDir>/projects/alpha/env/ exists after createProject");
|
|
|
+ fs::remove_all(dir);
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace
|
|
|
+
|
|
|
+int main() {
|
|
|
+ test_list_on_fresh_data_dir();
|
|
|
+ test_create_then_list_includes();
|
|
|
+ test_create_idempotent();
|
|
|
+ test_create_rejects_underscore_prefix();
|
|
|
+ test_create_rejects_digit_prefix();
|
|
|
+ test_drop_default_refused();
|
|
|
+ test_drop_then_list_excludes();
|
|
|
+ test_drop_nonexistent_refused();
|
|
|
+ test_create_writes_documented_layout();
|
|
|
+
|
|
|
+ std::cout << "project_crud: " << g_pass << " passed, "
|
|
|
+ << g_fail << " failed\n";
|
|
|
+ return g_fail == 0 ? 0 : 1;
|
|
|
+}
|