| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- /**
- * Unit tests for v1.7.0 T1: layered config loader (conf.d drop-ins).
- *
- * Exercises loadLayeredConfig() directly — no server.
- *
- * Test cases:
- * 1. Missing base + empty conf.d returns {}
- * 2. Base-only loads correctly
- * 3. Drop-in overrides base (deep merge preserves siblings)
- * 4. Drop-ins merged in lexicographic order (last wins)
- * 5. Arrays replace per RFC 7396 (not append)
- * 6. Invalid JSON in drop-in throws with filename in message
- * 7. Non-.json files in conf.d are ignored
- * 8. Missing conf.d directory is not an error
- */
- #include "../service/src/config/config_loader.hpp"
- #include <cassert>
- #include <filesystem>
- #include <fstream>
- #include <iostream>
- #include <string>
- #include <unistd.h>
- namespace fs = std::filesystem;
- using smartbotic::database::loadLayeredConfig;
- namespace {
- fs::path makeTestDir(const std::string& name) {
- auto dir = fs::temp_directory_path() / ("conf-test-" + name + "-" + std::to_string(::getpid()));
- fs::remove_all(dir);
- fs::create_directories(dir);
- return dir;
- }
- void writeFile(const fs::path& p, const std::string& content) {
- std::ofstream f(p);
- f << content;
- }
- } // anonymous namespace
- void test_no_base_no_dropins_returns_empty() {
- auto dir = makeTestDir("empty");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d";
- fs::create_directories(confDir);
- auto cfg = loadLayeredConfig(base, confDir);
- assert(cfg.is_object());
- assert(cfg.empty());
- fs::remove_all(dir);
- std::cout << "PASS: missing base + empty conf.d returns {}\n";
- }
- void test_base_only() {
- auto dir = makeTestDir("base");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d";
- fs::create_directories(confDir);
- writeFile(base, R"({"log_level":"info","storage":{"rpc_port":9004}})");
- auto cfg = loadLayeredConfig(base, confDir);
- assert(cfg["log_level"] == "info");
- assert(cfg["storage"]["rpc_port"] == 9004);
- fs::remove_all(dir);
- std::cout << "PASS: base-only config loads correctly\n";
- }
- void test_dropin_overrides_base() {
- auto dir = makeTestDir("override");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d";
- fs::create_directories(confDir);
- writeFile(base, R"({"log_level":"info","storage":{"rpc_port":9004,"node_id":"base"}})");
- writeFile(confDir / "50-override.json", R"({"log_level":"debug","storage":{"node_id":"overridden"}})");
- auto cfg = loadLayeredConfig(base, confDir);
- assert(cfg["log_level"] == "debug"); // overridden
- assert(cfg["storage"]["rpc_port"] == 9004); // preserved from base
- assert(cfg["storage"]["node_id"] == "overridden"); // overridden
- fs::remove_all(dir);
- std::cout << "PASS: drop-in overrides base (deep merge preserves siblings)\n";
- }
- void test_lexicographic_order() {
- auto dir = makeTestDir("order");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d";
- fs::create_directories(confDir);
- writeFile(base, R"({"log_level":"info"})");
- writeFile(confDir / "50-middle.json", R"({"log_level":"debug"})");
- writeFile(confDir / "10-first.json", R"({"log_level":"trace"})");
- writeFile(confDir / "99-last.json", R"({"log_level":"warn"})");
- auto cfg = loadLayeredConfig(base, confDir);
- // Order: base -> 10-first -> 50-middle -> 99-last. Last wins.
- assert(cfg["log_level"] == "warn");
- fs::remove_all(dir);
- std::cout << "PASS: drop-ins merged in lexicographic order, last wins\n";
- }
- void test_arrays_replace_not_append() {
- auto dir = makeTestDir("arrays");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d";
- fs::create_directories(confDir);
- writeFile(base, R"({"sensitive_fields":["email","phone"]})");
- writeFile(confDir / "50-override.json", R"({"sensitive_fields":["ssn"]})");
- auto cfg = loadLayeredConfig(base, confDir);
- // RFC 7396: arrays replace, don't merge
- assert(cfg["sensitive_fields"].size() == 1);
- assert(cfg["sensitive_fields"][0] == "ssn");
- fs::remove_all(dir);
- std::cout << "PASS: arrays replace (RFC 7396), don't append\n";
- }
- void test_invalid_json_throws() {
- auto dir = makeTestDir("invalid");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d";
- fs::create_directories(confDir);
- writeFile(base, R"({"log_level":"info"})");
- writeFile(confDir / "50-bad.json", R"({this is not json)");
- bool threw = false;
- try {
- (void)loadLayeredConfig(base, confDir);
- } catch (const std::exception& e) {
- threw = true;
- std::string msg = e.what();
- // Error should mention the filename so the operator knows which file
- assert(msg.find("50-bad.json") != std::string::npos);
- }
- assert(threw);
- fs::remove_all(dir);
- std::cout << "PASS: invalid JSON in drop-in throws with filename\n";
- }
- void test_non_json_files_ignored() {
- auto dir = makeTestDir("nonjson");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d";
- fs::create_directories(confDir);
- writeFile(base, R"({"log_level":"info"})");
- writeFile(confDir / "README.txt", "not a config file");
- writeFile(confDir / "backup.json.bak", R"({"log_level":"trace"})");
- writeFile(confDir / "50-real.json", R"({"log_level":"debug"})");
- auto cfg = loadLayeredConfig(base, confDir);
- assert(cfg["log_level"] == "debug"); // only 50-real.json should apply
- fs::remove_all(dir);
- std::cout << "PASS: non-json files in conf.d ignored\n";
- }
- void test_missing_confd_dir_is_ok() {
- auto dir = makeTestDir("nodir");
- auto base = dir / "config.json";
- auto confDir = dir / "conf.d"; // intentionally NOT created
- writeFile(base, R"({"log_level":"info"})");
- auto cfg = loadLayeredConfig(base, confDir);
- assert(cfg["log_level"] == "info"); // no drop-ins, base wins
- fs::remove_all(dir);
- std::cout << "PASS: missing conf.d directory is not an error\n";
- }
- int main() {
- test_no_base_no_dropins_returns_empty();
- test_base_only();
- test_dropin_overrides_base();
- test_lexicographic_order();
- test_arrays_replace_not_append();
- test_invalid_json_throws();
- test_non_json_files_ignored();
- test_missing_confd_dir_is_ok();
- std::cout << "\nAll config drop-in tests PASSED!\n";
- return 0;
- }
|