test_config_dropins.cpp 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. /**
  2. * Unit tests for v1.7.0 T1: layered config loader (conf.d drop-ins).
  3. *
  4. * Exercises loadLayeredConfig() directly — no server.
  5. *
  6. * Test cases:
  7. * 1. Missing base + empty conf.d returns {}
  8. * 2. Base-only loads correctly
  9. * 3. Drop-in overrides base (deep merge preserves siblings)
  10. * 4. Drop-ins merged in lexicographic order (last wins)
  11. * 5. Arrays replace per RFC 7396 (not append)
  12. * 6. Invalid JSON in drop-in throws with filename in message
  13. * 7. Non-.json files in conf.d are ignored
  14. * 8. Missing conf.d directory is not an error
  15. */
  16. #include "../service/src/config/config_loader.hpp"
  17. #include <cassert>
  18. #include <filesystem>
  19. #include <fstream>
  20. #include <iostream>
  21. #include <string>
  22. #include <unistd.h>
  23. namespace fs = std::filesystem;
  24. using smartbotic::database::loadLayeredConfig;
  25. namespace {
  26. fs::path makeTestDir(const std::string& name) {
  27. auto dir = fs::temp_directory_path() / ("conf-test-" + name + "-" + std::to_string(::getpid()));
  28. fs::remove_all(dir);
  29. fs::create_directories(dir);
  30. return dir;
  31. }
  32. void writeFile(const fs::path& p, const std::string& content) {
  33. std::ofstream f(p);
  34. f << content;
  35. }
  36. } // anonymous namespace
  37. void test_no_base_no_dropins_returns_empty() {
  38. auto dir = makeTestDir("empty");
  39. auto base = dir / "config.json";
  40. auto confDir = dir / "conf.d";
  41. fs::create_directories(confDir);
  42. auto cfg = loadLayeredConfig(base, confDir);
  43. assert(cfg.is_object());
  44. assert(cfg.empty());
  45. fs::remove_all(dir);
  46. std::cout << "PASS: missing base + empty conf.d returns {}\n";
  47. }
  48. void test_base_only() {
  49. auto dir = makeTestDir("base");
  50. auto base = dir / "config.json";
  51. auto confDir = dir / "conf.d";
  52. fs::create_directories(confDir);
  53. writeFile(base, R"({"log_level":"info","storage":{"rpc_port":9004}})");
  54. auto cfg = loadLayeredConfig(base, confDir);
  55. assert(cfg["log_level"] == "info");
  56. assert(cfg["storage"]["rpc_port"] == 9004);
  57. fs::remove_all(dir);
  58. std::cout << "PASS: base-only config loads correctly\n";
  59. }
  60. void test_dropin_overrides_base() {
  61. auto dir = makeTestDir("override");
  62. auto base = dir / "config.json";
  63. auto confDir = dir / "conf.d";
  64. fs::create_directories(confDir);
  65. writeFile(base, R"({"log_level":"info","storage":{"rpc_port":9004,"node_id":"base"}})");
  66. writeFile(confDir / "50-override.json", R"({"log_level":"debug","storage":{"node_id":"overridden"}})");
  67. auto cfg = loadLayeredConfig(base, confDir);
  68. assert(cfg["log_level"] == "debug"); // overridden
  69. assert(cfg["storage"]["rpc_port"] == 9004); // preserved from base
  70. assert(cfg["storage"]["node_id"] == "overridden"); // overridden
  71. fs::remove_all(dir);
  72. std::cout << "PASS: drop-in overrides base (deep merge preserves siblings)\n";
  73. }
  74. void test_lexicographic_order() {
  75. auto dir = makeTestDir("order");
  76. auto base = dir / "config.json";
  77. auto confDir = dir / "conf.d";
  78. fs::create_directories(confDir);
  79. writeFile(base, R"({"log_level":"info"})");
  80. writeFile(confDir / "50-middle.json", R"({"log_level":"debug"})");
  81. writeFile(confDir / "10-first.json", R"({"log_level":"trace"})");
  82. writeFile(confDir / "99-last.json", R"({"log_level":"warn"})");
  83. auto cfg = loadLayeredConfig(base, confDir);
  84. // Order: base -> 10-first -> 50-middle -> 99-last. Last wins.
  85. assert(cfg["log_level"] == "warn");
  86. fs::remove_all(dir);
  87. std::cout << "PASS: drop-ins merged in lexicographic order, last wins\n";
  88. }
  89. void test_arrays_replace_not_append() {
  90. auto dir = makeTestDir("arrays");
  91. auto base = dir / "config.json";
  92. auto confDir = dir / "conf.d";
  93. fs::create_directories(confDir);
  94. writeFile(base, R"({"sensitive_fields":["email","phone"]})");
  95. writeFile(confDir / "50-override.json", R"({"sensitive_fields":["ssn"]})");
  96. auto cfg = loadLayeredConfig(base, confDir);
  97. // RFC 7396: arrays replace, don't merge
  98. assert(cfg["sensitive_fields"].size() == 1);
  99. assert(cfg["sensitive_fields"][0] == "ssn");
  100. fs::remove_all(dir);
  101. std::cout << "PASS: arrays replace (RFC 7396), don't append\n";
  102. }
  103. void test_invalid_json_throws() {
  104. auto dir = makeTestDir("invalid");
  105. auto base = dir / "config.json";
  106. auto confDir = dir / "conf.d";
  107. fs::create_directories(confDir);
  108. writeFile(base, R"({"log_level":"info"})");
  109. writeFile(confDir / "50-bad.json", R"({this is not json)");
  110. bool threw = false;
  111. try {
  112. (void)loadLayeredConfig(base, confDir);
  113. } catch (const std::exception& e) {
  114. threw = true;
  115. std::string msg = e.what();
  116. // Error should mention the filename so the operator knows which file
  117. assert(msg.find("50-bad.json") != std::string::npos);
  118. }
  119. assert(threw);
  120. fs::remove_all(dir);
  121. std::cout << "PASS: invalid JSON in drop-in throws with filename\n";
  122. }
  123. void test_non_json_files_ignored() {
  124. auto dir = makeTestDir("nonjson");
  125. auto base = dir / "config.json";
  126. auto confDir = dir / "conf.d";
  127. fs::create_directories(confDir);
  128. writeFile(base, R"({"log_level":"info"})");
  129. writeFile(confDir / "README.txt", "not a config file");
  130. writeFile(confDir / "backup.json.bak", R"({"log_level":"trace"})");
  131. writeFile(confDir / "50-real.json", R"({"log_level":"debug"})");
  132. auto cfg = loadLayeredConfig(base, confDir);
  133. assert(cfg["log_level"] == "debug"); // only 50-real.json should apply
  134. fs::remove_all(dir);
  135. std::cout << "PASS: non-json files in conf.d ignored\n";
  136. }
  137. void test_missing_confd_dir_is_ok() {
  138. auto dir = makeTestDir("nodir");
  139. auto base = dir / "config.json";
  140. auto confDir = dir / "conf.d"; // intentionally NOT created
  141. writeFile(base, R"({"log_level":"info"})");
  142. auto cfg = loadLayeredConfig(base, confDir);
  143. assert(cfg["log_level"] == "info"); // no drop-ins, base wins
  144. fs::remove_all(dir);
  145. std::cout << "PASS: missing conf.d directory is not an error\n";
  146. }
  147. int main() {
  148. test_no_base_no_dropins_returns_empty();
  149. test_base_only();
  150. test_dropin_overrides_base();
  151. test_lexicographic_order();
  152. test_arrays_replace_not_append();
  153. test_invalid_json_throws();
  154. test_non_json_files_ignored();
  155. test_missing_confd_dir_is_ok();
  156. std::cout << "\nAll config drop-in tests PASSED!\n";
  157. return 0;
  158. }