// v2.3 — project-addressing parser tests. // // Locks in the edge-case semantics. Anything that relies on the parser // (gRPC handlers, mirror routing, client) builds on these guarantees. #include #include #include #include "project_addressing.hpp" using smartbotic::database::kDefaultProject; using smartbotic::database::ProjectCollection; using smartbotic::database::isValidProjectName; using smartbotic::database::parseProjectCollection; 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"; } } template void check_throws(Fn fn, const char* msg) { try { fn(); ++g_fail; std::cerr << "FAIL (did not throw): " << msg << "\n"; } catch (const std::invalid_argument&) { ++g_pass; } catch (...) { ++g_fail; std::cerr << "FAIL (wrong exception type): " << msg << "\n"; } } void test_unqualified_resolves_to_default() { auto r = parseProjectCollection("users"); check(r.project == kDefaultProject, "unqualified: project == default"); check(r.collection == "users", "unqualified: collection preserved"); } void test_qualified_splits_on_separator() { auto r = parseProjectCollection("my_app:users"); check(r.project == "my_app", "qualified: project parsed"); check(r.collection == "users", "qualified: collection parsed"); } void test_long_collection_name() { auto r = parseProjectCollection("my_app:some_long_collection_name"); check(r.collection == "some_long_collection_name", "qualified: long collection preserved"); } void test_empty_input_throws() { check_throws([] { parseProjectCollection(""); }, "empty input must throw"); } void test_leading_colon_throws() { check_throws([] { parseProjectCollection(":users"); }, "leading ':' must throw (empty project)"); } void test_trailing_colon_throws() { check_throws([] { parseProjectCollection("my_app:"); }, "trailing ':' must throw (empty collection)"); } void test_double_separator_throws() { check_throws([] { parseProjectCollection("a:b:c"); }, "double ':' must throw (ambiguous)"); } void test_just_colon_throws() { check_throws([] { parseProjectCollection(":"); }, "':' alone must throw"); } void test_valid_project_names() { check(isValidProjectName("default"), "'default' is valid"); check(isValidProjectName("my_app"), "'my_app' is valid"); check(isValidProjectName("App1"), "'App1' is valid"); check(isValidProjectName("a-b-c"), "'a-b-c' is valid (dash allowed)"); check(isValidProjectName("_starts_underscore") == false, "'_-prefix is reserved (system)"); check(isValidProjectName("") == false, "empty is invalid"); check(isValidProjectName("9starts_digit") == false, "digit-start is invalid"); check(isValidProjectName("has:colon") == false, "':' inside name is invalid"); check(isValidProjectName("has/slash") == false, "'/' inside name is invalid (filesystem safety)"); check(isValidProjectName("has.dot") == false, "'.' inside name is invalid"); check(isValidProjectName("has space") == false, "whitespace inside name is invalid"); std::string too_long(64, 'a'); check(isValidProjectName(too_long) == false, "64+ chars exceeds kMaxProjectNameLen"); std::string max_len(63, 'a'); check(isValidProjectName(max_len), "63 chars is the boundary"); } } // namespace int main() { test_unqualified_resolves_to_default(); test_qualified_splits_on_separator(); test_long_collection_name(); test_empty_input_throws(); test_leading_colon_throws(); test_trailing_colon_throws(); test_double_separator_throws(); test_just_colon_throws(); test_valid_project_names(); std::cout << "project_addressing: " << g_pass << " passed, " << g_fail << " failed\n"; return g_fail == 0 ? 0 : 1; }