// v2.3 Stage F — project CRUD placeholder tests. // // The placeholder is filesystem-only (under /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 #include #include #include #include #include // 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& 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: // /projects//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"), "/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; }