// test_http_get_json.cpp — Catch2 integration tests for fn_http::get_json. // Issue 0110. Uses the same Python HTTP fixture as test_http_request.cpp but // scoped locally to keep tests independent (each binary owns its own server). #include "catch_amalgamated.hpp" #include "core/http_get_json.h" #include "core/http_request.h" #include "nlohmann/json.hpp" #include #include #include #include #include #include #ifndef _WIN32 #include #include #include #endif namespace { constexpr const char* kHost = "127.0.0.1"; int pick_port() { return 47300 + (int)(getpid() % 100); } struct PythonServer { pid_t pid = 0; int port = 0; bool start() { port = pick_port(); std::string script_path = std::tmpnam(nullptr); script_path += ".py"; { std::ofstream f(script_path); f << "import json, sys\n" "from http.server import BaseHTTPRequestHandler, HTTPServer\n" "PORT = int(sys.argv[1])\n" "class H(BaseHTTPRequestHandler):\n" " def log_message(self, *a, **k): pass\n" " def do_GET(self):\n" " if self.path.startswith('/status/'):\n" " code = int(self.path.rsplit('/',1)[1])\n" " msg = ('error %d' % code).encode()\n" " self.send_response(code); self.send_header('content-length',str(len(msg))); self.end_headers(); self.wfile.write(msg); return\n" " if self.path == '/notjson':\n" " self.send_response(200); self.send_header('content-type','text/plain'); self.end_headers(); self.wfile.write(b'<>'); return\n" " payload = {\n" " 'ok':True,\n" " 'path':self.path,\n" " 'auth':self.headers.get('Authorization','')\n" " }\n" " out = json.dumps(payload).encode()\n" " self.send_response(200); self.send_header('content-type','application/json'); self.send_header('content-length',str(len(out))); self.end_headers(); self.wfile.write(out)\n" "HTTPServer(('127.0.0.1', PORT), H).serve_forever()\n"; } #ifdef _WIN32 return false; #else pid_t p = fork(); if (p < 0) return false; if (p == 0) { FILE* dn = std::freopen("/dev/null", "r", stdin); (void)dn; FILE* d2 = std::freopen("/dev/null", "w", stdout); (void)d2; FILE* d3 = std::freopen("/dev/null", "w", stderr); (void)d3; execlp("python3", "python3", "-u", script_path.c_str(), std::to_string(port).c_str(), (char*)nullptr); std::_Exit(127); } pid = p; for (int i = 0; i < 60; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); fn_http::Request r; r.url = std::string("http://") + kHost + ":" + std::to_string(port) + "/ping"; r.timeout_ms = 500; auto res = fn_http::request(r); if (res.status == 200) return true; } return false; #endif } void stop() { #ifndef _WIN32 if (pid > 0) { kill(pid, SIGTERM); int st = 0; waitpid(pid, &st, 0); pid = 0; } #endif } std::string base() const { return std::string("http://") + kHost + ":" + std::to_string(port); } }; PythonServer& server() { static PythonServer s; static bool inited = false; if (!inited) { inited = true; s.start(); std::atexit([] { server().stop(); }); } return s; } bool server_alive() { return server().pid > 0; } } // anon TEST_CASE("get_json: parses 200 JSON body", "[http_get_json]") { if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; } auto j = fn_http::get_json(server().base() + "/echo"); REQUIRE(j.is_object()); REQUIRE(j.at("ok").get() == true); REQUIRE(j.at("path").get() == "/echo"); } TEST_CASE("get_json: throws on 404", "[http_get_json]") { if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; } REQUIRE_THROWS_AS( fn_http::get_json(server().base() + "/status/404"), std::runtime_error); } TEST_CASE("get_json: bearer_token reaches server", "[http_get_json]") { if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; } auto j = fn_http::get_json(server().base() + "/whoami", "tok_42"); REQUIRE(j.at("auth").get() == "Bearer tok_42"); } TEST_CASE("get_json: throws on invalid JSON", "[http_get_json]") { if (!server_alive()) { SUCCEED("python3 fixture not available — skipped"); return; } REQUIRE_THROWS_AS( fn_http::get_json(server().base() + "/notjson"), std::runtime_error); }