api_fixture.hpp 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. #pragma once
  2. #include "db_test_util.hpp"
  3. #include "server.hpp"
  4. #include <gtest/gtest.h>
  5. #include <httplib.h>
  6. #include <chrono>
  7. #include <filesystem>
  8. #include <fstream>
  9. #include <memory>
  10. #include <thread>
  11. #include <vector>
  12. namespace svapi::testutil {
  13. class ApiFixture : public ::testing::Test {
  14. protected:
  15. std::unique_ptr<DbGateway> db_;
  16. std::unique_ptr<CollectionRegistry> registry_;
  17. std::unique_ptr<SettingsStore> settings_;
  18. std::unique_ptr<KeyStore> keys_;
  19. std::unique_ptr<SessionStore> sessions_;
  20. std::unique_ptr<ApiServer> server_;
  21. std::thread serverThread_;
  22. int port_ = 0;
  23. std::string adminKey_;
  24. std::string project_; // a project the admin owns; tests create collections under it
  25. httplib::Server mockOpenAi_;
  26. std::thread mockThread_;
  27. int mockPort_ = 0, mockDim_ = 4;
  28. // What the last outbound embedding request actually carried, so tests can
  29. // assert on the request we send to a provider, not just on our own helpers.
  30. std::string lastEmbedUserAgent_, lastEmbedAuth_;
  31. nlohmann::json lastEmbedBody_ = nlohmann::json::object();
  32. std::string settingsColl_ = "svapitest_api_settings";
  33. std::string keysColl_ = "svapitest_api_keys";
  34. std::string webuiDir_ = "/tmp/svapi_webui_test"; // sentinel SPA root for fallback tests
  35. void SetUp() override {
  36. db_ = connectOrNull();
  37. if (!db_) GTEST_SKIP() << "smartbotic-database not reachable";
  38. project_ = tmpName("apiproj");
  39. db_->client().dropCollection(settingsColl_);
  40. db_->client().dropCollection(keysColl_);
  41. db_->dropProject(project_);
  42. // DB v2.3.1 Stage-F caveat: dropProject does not delete collection data.
  43. // Drop the project's registry collection explicitly so ensure() starts fresh.
  44. db_->client().dropCollection(qualify(project_, "vectorapi_collections"));
  45. db_->ensureProject(project_);
  46. registry_ = std::make_unique<CollectionRegistry>(*db_);
  47. registry_->ensure(project_);
  48. settings_ = std::make_unique<SettingsStore>(*db_, settingsColl_, "current");
  49. settings_->bootstrap("");
  50. keys_ = std::make_unique<KeyStore>(*db_, keysColl_);
  51. keys_->bootstrap("");
  52. adminKey_ = keys_->list().at(0).key;
  53. sessions_ = std::make_unique<SessionStore>(std::chrono::minutes(60));
  54. mockOpenAi_.Post("/v1/embeddings", [this](const httplib::Request& req, httplib::Response& res) {
  55. lastEmbedUserAgent_ = req.get_header_value("User-Agent");
  56. lastEmbedAuth_ = req.get_header_value("Authorization");
  57. try { lastEmbedBody_ = nlohmann::json::parse(req.body); } catch (...) {}
  58. nlohmann::json emb = nlohmann::json::array();
  59. for (int i = 0; i < mockDim_; ++i) emb.push_back(0.1f * (i + 1));
  60. res.set_content(nlohmann::json{{"data", {{{"embedding", emb}}}}}.dump(), "application/json");
  61. });
  62. mockPort_ = mockOpenAi_.bind_to_any_port("127.0.0.1");
  63. mockThread_ = std::thread([this]{ mockOpenAi_.listen_after_bind(); });
  64. Settings s = *settings_->snapshot();
  65. s.openaiApiBase = "http://127.0.0.1:" + std::to_string(mockPort_);
  66. s.openaiApiKey = "test";
  67. settings_->save(s);
  68. std::filesystem::create_directories(webuiDir_);
  69. std::ofstream(webuiDir_ + "/index.html")
  70. << "<!doctype html><html><head><title>SVAPI_SPA_OK</title></head><body><div id=\"root\"></div></body></html>";
  71. ServerDeps deps{*db_, *settings_, *keys_, *registry_, *sessions_, webuiDir_, apiDocsDir()};
  72. server_ = std::make_unique<ApiServer>(std::move(deps));
  73. server_->registerRoutes();
  74. port_ = server_->bindToAnyPort("127.0.0.1");
  75. serverThread_ = std::thread([this]{ server_->listenAfterBind(); });
  76. httplib::Client probe("127.0.0.1", port_);
  77. for (int i = 0; i < 200; ++i) { if (probe.Get("/healthz")) break;
  78. std::this_thread::sleep_for(std::chrono::milliseconds(10)); }
  79. }
  80. void TearDown() override {
  81. if (server_) server_->stop();
  82. if (serverThread_.joinable()) serverThread_.join();
  83. mockOpenAi_.stop();
  84. if (mockThread_.joinable()) mockThread_.join();
  85. if (db_) { db_->client().dropCollection(settingsColl_);
  86. db_->client().dropCollection(keysColl_);
  87. db_->dropProject(project_); }
  88. }
  89. httplib::Client http(const std::string& bearer) {
  90. httplib::Client c("127.0.0.1", port_);
  91. c.set_default_headers({{"Authorization", "Bearer " + bearer}});
  92. return c;
  93. }
  94. httplib::Client admin() { return http(adminKey_); }
  95. httplib::Client noAuth() { return httplib::Client("127.0.0.1", port_); }
  96. static std::string apiDocsDir() { return SVAPI_API_DOCS_DIR; }
  97. // Send a single request with a specific key and extra headers (for origin/cache-control tests).
  98. // method: "GET", "POST", "PATCH", "PUT", "DELETE".
  99. // body/contentType: pass "" for requests without a body.
  100. httplib::Result request(const std::string& method, const std::string& path,
  101. const std::string& body, const std::string& contentType,
  102. const std::string& key,
  103. const httplib::Headers& extraHeaders = {}) {
  104. httplib::Headers hdrs = {{"Authorization", "Bearer " + key}};
  105. for (const auto& [k, v] : extraHeaders) hdrs.emplace(k, v);
  106. httplib::Client c("127.0.0.1", port_);
  107. c.set_default_headers(hdrs);
  108. if (method == "GET") return c.Get(path.c_str());
  109. if (method == "DELETE") return c.Delete(path.c_str());
  110. if (method == "POST") return c.Post(path.c_str(), body, contentType.c_str());
  111. if (method == "PUT") return c.Put(path.c_str(), body, contentType.c_str());
  112. if (method == "PATCH") return c.Patch(path.c_str(), body, contentType.c_str());
  113. throw std::invalid_argument("unsupported method: " + method);
  114. }
  115. };
  116. } // namespace svapi::testutil