| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- // 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;
- }
|