6f3c129a14
Catch2-based tests that fork+exec a local python3 http.server fixture per
test binary. Covers:
http_request:
- GET 200 with body
- GET 404 (HTTP error != transport error)
- POST with body + Content-Type
- bearer_token shortcut adds Authorization: Bearer
- basic_user/basic_pass shortcut adds HTTP Basic (curl --user)
- invalid URL surfaces transport error (status=0)
- timeout_ms is honored (bails before server's 3s sleep)
http_get_json:
- parses 200 JSON body
- throws std::runtime_error on 404
- bearer_token reaches server (verified via echoed Authorization header)
- throws std::runtime_error on invalid JSON body
Tests skip gracefully if python3 isn't available (server.start() returns
false; SUCCEED with skip message). No external network required.
Local runs (Linux): 21 assertions / 7 cases (http_request), 6 / 4 (get_json),
all passing.
146 lines
5.0 KiB
C++
146 lines
5.0 KiB
C++
// 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 <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
#ifndef _WIN32
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#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'<<not json>>'); 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<bool>() == true);
|
|
REQUIRE(j.at("path").get<std::string>() == "/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<std::string>() == "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);
|
|
}
|