min boilerplate 1/2
This commit is contained in:
parent
e02c295dac
commit
d383a4a543
@ -41,13 +41,15 @@ FetchContent_MakeAvailable(cli11 tomlplusplus Catch2)
|
||||
add_subdirectory(packages/logger)
|
||||
add_subdirectory(packages/html)
|
||||
add_subdirectory(packages/postgres)
|
||||
add_subdirectory(packages/http)
|
||||
add_subdirectory(packages/json)
|
||||
|
||||
# ── Sources ──────────────────────────────────────────────────────────────────
|
||||
add_executable(${PROJECT_NAME}
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres http json)
|
||||
|
||||
# ── Compiler warnings ───────────────────────────────────────────────────────
|
||||
if(MSVC)
|
||||
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "mono-cpp",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "mono-cpp",
|
||||
"version": "1.0.0",
|
||||
"description": "Cross-platform C++ CLI built with CMake.",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"config": "cmake --preset dev",
|
||||
"config:release": "cmake --preset release",
|
||||
"build": "cmake --build --preset dev",
|
||||
"build:release": "cmake --build --preset release",
|
||||
"test": "ctest --test-dir build/dev -C Debug --output-on-failure",
|
||||
"test:release": "ctest --test-dir build/release -C Release --output-on-failure",
|
||||
"clean": "cmake -E rm -rf build/dev",
|
||||
"clean:release": "cmake -E rm -rf build/release",
|
||||
"clean:all": "cmake -E rm -rf build",
|
||||
"rebuild": "npm run clean && npm run config && npm run build",
|
||||
"run": ".\\build\\dev\\Debug\\polymech-cli.exe"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.polymech.info/polymech/mono-cpp.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
43
packages/http/CMakeLists.txt
Normal file
43
packages/http/CMakeLists.txt
Normal file
@ -0,0 +1,43 @@
|
||||
include(FetchContent)
|
||||
|
||||
# Work around curl's old cmake_minimum_required for CMake 4.x
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
|
||||
|
||||
FetchContent_Declare(
|
||||
CURL
|
||||
URL https://github.com/curl/curl/releases/download/curl-8_12_1/curl-8.12.1.tar.xz
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
|
||||
# Minimal curl build — static, SChannel TLS, no optional deps
|
||||
set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# TLS backend: Windows native SChannel
|
||||
set(CURL_USE_OPENSSL OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE)
|
||||
|
||||
# Disable optional compression/protocol deps
|
||||
set(CURL_ZLIB OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_BROTLI OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_ZSTD OFF CACHE BOOL "" FORCE)
|
||||
set(USE_NGHTTP2 OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE)
|
||||
set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE)
|
||||
|
||||
FetchContent_MakeAvailable(CURL)
|
||||
|
||||
add_library(http STATIC
|
||||
src/http.cpp
|
||||
)
|
||||
|
||||
target_include_directories(http
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(http
|
||||
PUBLIC CURL::libcurl
|
||||
)
|
||||
19
packages/http/include/http/http.h
Normal file
19
packages/http/include/http/http.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace http {
|
||||
|
||||
struct Response {
|
||||
long status_code;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
/// Perform an HTTP GET request. Returns the response body and status code.
|
||||
Response get(const std::string &url);
|
||||
|
||||
/// Perform an HTTP POST request with a body. Returns the response and status.
|
||||
Response post(const std::string &url, const std::string &body,
|
||||
const std::string &content_type = "application/json");
|
||||
|
||||
} // namespace http
|
||||
77
packages/http/src/http.cpp
Normal file
77
packages/http/src/http.cpp
Normal file
@ -0,0 +1,77 @@
|
||||
#include "http/http.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
namespace http {
|
||||
|
||||
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp) {
|
||||
auto *out = static_cast<std::string *>(userp);
|
||||
out->append(static_cast<char *>(contents), size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
Response get(const std::string &url) {
|
||||
Response resp{};
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
resp.status_code = -1;
|
||||
resp.body = "curl_easy_init failed";
|
||||
return resp;
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp.body);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK) {
|
||||
resp.status_code = -1;
|
||||
resp.body = curl_easy_strerror(res);
|
||||
} else {
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp.status_code);
|
||||
}
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
return resp;
|
||||
}
|
||||
|
||||
Response post(const std::string &url, const std::string &body,
|
||||
const std::string &content_type) {
|
||||
Response resp{};
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
resp.status_code = -1;
|
||||
resp.body = "curl_easy_init failed";
|
||||
return resp;
|
||||
}
|
||||
|
||||
struct curl_slist *headers = nullptr;
|
||||
headers =
|
||||
curl_slist_append(headers, ("Content-Type: " + content_type).c_str());
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp.body);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK) {
|
||||
resp.status_code = -1;
|
||||
resp.body = curl_easy_strerror(res);
|
||||
} else {
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &resp.status_code);
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
return resp;
|
||||
}
|
||||
|
||||
} // namespace http
|
||||
28
packages/json/CMakeLists.txt
Normal file
28
packages/json/CMakeLists.txt
Normal file
@ -0,0 +1,28 @@
|
||||
include(FetchContent)
|
||||
|
||||
# RapidJSON — use master for CMake 4.x compatibility (v1.1.0 is from 2016)
|
||||
FetchContent_Declare(
|
||||
rapidjson
|
||||
GIT_REPOSITORY https://github.com/Tencent/rapidjson.git
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
set(RAPIDJSON_BUILD_DOC OFF CACHE BOOL "" FORCE)
|
||||
set(RAPIDJSON_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(RAPIDJSON_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
FetchContent_GetProperties(rapidjson)
|
||||
if(NOT rapidjson_POPULATED)
|
||||
FetchContent_Populate(rapidjson)
|
||||
# Don't add_subdirectory — just use the headers
|
||||
endif()
|
||||
|
||||
add_library(json STATIC
|
||||
src/json.cpp
|
||||
)
|
||||
|
||||
target_include_directories(json
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
PUBLIC ${rapidjson_SOURCE_DIR}/include
|
||||
)
|
||||
23
packages/json/include/json/json.h
Normal file
23
packages/json/include/json/json.h
Normal file
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace json {
|
||||
|
||||
/// Parse a JSON string and return a pretty-printed version.
|
||||
std::string prettify(const std::string &json_str);
|
||||
|
||||
/// Extract a string value by key from a JSON object (top-level only).
|
||||
std::string get_string(const std::string &json_str, const std::string &key);
|
||||
|
||||
/// Extract an int value by key from a JSON object (top-level only).
|
||||
int get_int(const std::string &json_str, const std::string &key);
|
||||
|
||||
/// Check if a JSON string is valid.
|
||||
bool is_valid(const std::string &json_str);
|
||||
|
||||
/// Get all top-level keys from a JSON object.
|
||||
std::vector<std::string> keys(const std::string &json_str);
|
||||
|
||||
} // namespace json
|
||||
62
packages/json/src/json.cpp
Normal file
62
packages/json/src/json.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
#include "json/json.h"
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/prettywriter.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
|
||||
namespace json {
|
||||
|
||||
std::string prettify(const std::string &json_str) {
|
||||
rapidjson::Document doc;
|
||||
doc.Parse(json_str.c_str());
|
||||
if (doc.HasParseError()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
rapidjson::StringBuffer buffer;
|
||||
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
|
||||
doc.Accept(writer);
|
||||
return std::string(buffer.GetString(), buffer.GetSize());
|
||||
}
|
||||
|
||||
std::string get_string(const std::string &json_str, const std::string &key) {
|
||||
rapidjson::Document doc;
|
||||
doc.Parse(json_str.c_str());
|
||||
if (doc.HasParseError() || !doc.IsObject())
|
||||
return {};
|
||||
auto it = doc.FindMember(key.c_str());
|
||||
if (it == doc.MemberEnd() || !it->value.IsString())
|
||||
return {};
|
||||
return std::string(it->value.GetString(), it->value.GetStringLength());
|
||||
}
|
||||
|
||||
int get_int(const std::string &json_str, const std::string &key) {
|
||||
rapidjson::Document doc;
|
||||
doc.Parse(json_str.c_str());
|
||||
if (doc.HasParseError() || !doc.IsObject())
|
||||
return 0;
|
||||
auto it = doc.FindMember(key.c_str());
|
||||
if (it == doc.MemberEnd() || !it->value.IsInt())
|
||||
return 0;
|
||||
return it->value.GetInt();
|
||||
}
|
||||
|
||||
bool is_valid(const std::string &json_str) {
|
||||
rapidjson::Document doc;
|
||||
doc.Parse(json_str.c_str());
|
||||
return !doc.HasParseError();
|
||||
}
|
||||
|
||||
std::vector<std::string> keys(const std::string &json_str) {
|
||||
std::vector<std::string> result;
|
||||
rapidjson::Document doc;
|
||||
doc.Parse(json_str.c_str());
|
||||
if (doc.HasParseError() || !doc.IsObject())
|
||||
return result;
|
||||
for (auto it = doc.MemberBegin(); it != doc.MemberEnd(); ++it) {
|
||||
result.emplace_back(it->name.GetString(), it->name.GetStringLength());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
35
src/main.cpp
35
src/main.cpp
@ -5,8 +5,11 @@
|
||||
#include <toml++/toml.hpp>
|
||||
|
||||
#include "html/html.h"
|
||||
#include "http/http.h"
|
||||
#include "logger/logger.h"
|
||||
#include "postgres/postgres.h"
|
||||
#include "json/json.h"
|
||||
|
||||
|
||||
#ifndef PROJECT_VERSION
|
||||
#define PROJECT_VERSION "0.1.0"
|
||||
@ -35,6 +38,17 @@ int main(int argc, char *argv[]) {
|
||||
app.add_subcommand("config", "Read and display a TOML config file");
|
||||
config_cmd->add_option("file", config_path, "Path to TOML file")->required();
|
||||
|
||||
// Subcommand: fetch — HTTP GET a URL
|
||||
std::string fetch_url;
|
||||
auto *fetch_cmd =
|
||||
app.add_subcommand("fetch", "HTTP GET a URL and print the response");
|
||||
fetch_cmd->add_option("url", fetch_url, "URL to fetch")->required();
|
||||
|
||||
// Subcommand: json — prettify JSON
|
||||
std::string json_input;
|
||||
auto *json_cmd = app.add_subcommand("json", "Prettify a JSON string");
|
||||
json_cmd->add_option("input", json_input, "JSON string")->required();
|
||||
|
||||
CLI11_PARSE(app, argc, argv);
|
||||
|
||||
logger::init("polymech-cli");
|
||||
@ -69,6 +83,27 @@ int main(int argc, char *argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fetch_cmd->parsed()) {
|
||||
auto resp = http::get(fetch_url);
|
||||
logger::info("HTTP " + std::to_string(resp.status_code) + " from " +
|
||||
fetch_url);
|
||||
if (json::is_valid(resp.body)) {
|
||||
std::cout << json::prettify(resp.body) << "\n";
|
||||
} else {
|
||||
std::cout << resp.body << "\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (json_cmd->parsed()) {
|
||||
if (!json::is_valid(json_input)) {
|
||||
logger::error("Invalid JSON input");
|
||||
return 1;
|
||||
}
|
||||
std::cout << json::prettify(json_input) << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Default: demo
|
||||
auto status = postgres::connect("postgresql://localhost:5432/polymech");
|
||||
logger::info("polymech-cli " + std::string(PROJECT_VERSION) +
|
||||
|
||||
29
tests/CMakeLists.txt
Normal file
29
tests/CMakeLists.txt
Normal file
@ -0,0 +1,29 @@
|
||||
# ── Test targets ──────────────────────────────────────────────────────────────
|
||||
include(CTest)
|
||||
include(Catch)
|
||||
|
||||
# Unit tests — one per package
|
||||
add_executable(test_logger unit/test_logger.cpp)
|
||||
target_link_libraries(test_logger PRIVATE Catch2::Catch2WithMain logger)
|
||||
catch_discover_tests(test_logger)
|
||||
|
||||
add_executable(test_html unit/test_html.cpp)
|
||||
target_link_libraries(test_html PRIVATE Catch2::Catch2WithMain html)
|
||||
catch_discover_tests(test_html)
|
||||
|
||||
add_executable(test_postgres unit/test_postgres.cpp)
|
||||
target_link_libraries(test_postgres PRIVATE Catch2::Catch2WithMain postgres)
|
||||
catch_discover_tests(test_postgres)
|
||||
|
||||
add_executable(test_json unit/test_json.cpp)
|
||||
target_link_libraries(test_json PRIVATE Catch2::Catch2WithMain json)
|
||||
catch_discover_tests(test_json)
|
||||
|
||||
add_executable(test_http unit/test_http.cpp)
|
||||
target_link_libraries(test_http PRIVATE Catch2::Catch2WithMain http)
|
||||
catch_discover_tests(test_http)
|
||||
|
||||
# Functional test — end-to-end CLI
|
||||
add_executable(test_functional functional/test_cli.cpp)
|
||||
target_link_libraries(test_functional PRIVATE Catch2::Catch2WithMain CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres http json)
|
||||
catch_discover_tests(test_functional)
|
||||
72
tests/functional/test_cli.cpp
Normal file
72
tests/functional/test_cli.cpp
Normal file
@ -0,0 +1,72 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include <toml++/toml.hpp>
|
||||
|
||||
#include "html/html.h"
|
||||
#include "logger/logger.h"
|
||||
#include "postgres/postgres.h"
|
||||
|
||||
// ── Functional: full pipeline tests ─────────────────────────────────────────
|
||||
|
||||
TEST_CASE("Full pipeline: parse HTML and select", "[functional]") {
|
||||
const std::string input =
|
||||
"<html><body>"
|
||||
"<h1>Title</h1>"
|
||||
"<ul><li class=\"item\">A</li><li class=\"item\">B</li></ul>"
|
||||
"</body></html>";
|
||||
|
||||
// Parse should find elements
|
||||
auto elements = html::parse(input);
|
||||
REQUIRE(!elements.empty());
|
||||
|
||||
// Select by class should find 2 items
|
||||
auto items = html::select(input, ".item");
|
||||
REQUIRE(items.size() == 2);
|
||||
CHECK(items[0] == "A");
|
||||
CHECK(items[1] == "B");
|
||||
}
|
||||
|
||||
TEST_CASE("Full pipeline: TOML config round-trip", "[functional]") {
|
||||
// Write a temp TOML file
|
||||
const std::string toml_content = "[server]\n"
|
||||
"host = \"localhost\"\n"
|
||||
"port = 8080\n"
|
||||
"\n"
|
||||
"[database]\n"
|
||||
"name = \"test_db\"\n";
|
||||
|
||||
std::string tmp_path = "test_config_tmp.toml";
|
||||
{
|
||||
std::ofstream out(tmp_path);
|
||||
REQUIRE(out.is_open());
|
||||
out << toml_content;
|
||||
}
|
||||
|
||||
// Parse it
|
||||
auto tbl = toml::parse_file(tmp_path);
|
||||
|
||||
CHECK(tbl["server"]["host"].value_or("") == std::string("localhost"));
|
||||
CHECK(tbl["server"]["port"].value_or(0) == 8080);
|
||||
CHECK(tbl["database"]["name"].value_or("") == std::string("test_db"));
|
||||
|
||||
// Serialize back
|
||||
std::ostringstream ss;
|
||||
ss << tbl;
|
||||
auto serialized = ss.str();
|
||||
CHECK(serialized.find("localhost") != std::string::npos);
|
||||
|
||||
// Cleanup
|
||||
std::remove(tmp_path.c_str());
|
||||
}
|
||||
|
||||
TEST_CASE("Full pipeline: logger + postgres integration", "[functional]") {
|
||||
REQUIRE_NOTHROW(logger::init("functional-test"));
|
||||
|
||||
auto status = postgres::connect("postgresql://localhost:5432/functional");
|
||||
REQUIRE(status == "ok");
|
||||
|
||||
REQUIRE_NOTHROW(
|
||||
logger::info("Functional test passed with pg status: " + status));
|
||||
}
|
||||
63
tests/unit/test_html.cpp
Normal file
63
tests/unit/test_html.cpp
Normal file
@ -0,0 +1,63 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "html/html.h"
|
||||
|
||||
TEST_CASE("html::parse returns elements from valid HTML", "[html]") {
|
||||
auto elements =
|
||||
html::parse("<html><body><h1>Title</h1><p>Body</p></body></html>");
|
||||
|
||||
REQUIRE(elements.size() >= 2);
|
||||
|
||||
bool found_h1 = false;
|
||||
bool found_p = false;
|
||||
for (const auto &el : elements) {
|
||||
if (el.tag == "h1" && el.text == "Title")
|
||||
found_h1 = true;
|
||||
if (el.tag == "p" && el.text == "Body")
|
||||
found_p = true;
|
||||
}
|
||||
CHECK(found_h1);
|
||||
CHECK(found_p);
|
||||
}
|
||||
|
||||
TEST_CASE("html::parse returns empty for empty input", "[html]") {
|
||||
auto elements = html::parse("");
|
||||
// Empty or minimal — parser may produce an empty body
|
||||
REQUIRE(elements.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("html::parse handles nested elements", "[html]") {
|
||||
auto elements = html::parse("<div><span>Nested</span></div>");
|
||||
|
||||
// Parent nodes (body, div) also get text "Nested" via node_text.
|
||||
// Just verify that the span element is present among the results.
|
||||
bool found_span = false;
|
||||
for (const auto &el : elements) {
|
||||
if (el.tag == "span" && el.text == "Nested") {
|
||||
found_span = true;
|
||||
}
|
||||
}
|
||||
CHECK(found_span);
|
||||
}
|
||||
|
||||
TEST_CASE("html::select finds elements by CSS selector", "[html][select]") {
|
||||
auto matches = html::select("<ul><li>A</li><li>B</li><li>C</li></ul>", "li");
|
||||
|
||||
REQUIRE(matches.size() == 3);
|
||||
CHECK(matches[0] == "A");
|
||||
CHECK(matches[1] == "B");
|
||||
CHECK(matches[2] == "C");
|
||||
}
|
||||
|
||||
TEST_CASE("html::select returns empty for no matches", "[html][select]") {
|
||||
auto matches = html::select("<p>Hello</p>", "h1");
|
||||
REQUIRE(matches.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("html::select works with class selector", "[html][select]") {
|
||||
auto matches = html::select(
|
||||
R"(<div><span class="a">X</span><span class="b">Y</span></div>)", ".a");
|
||||
|
||||
REQUIRE(matches.size() == 1);
|
||||
CHECK(matches[0] == "X");
|
||||
}
|
||||
17
tests/unit/test_http.cpp
Normal file
17
tests/unit/test_http.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "http/http.h"
|
||||
|
||||
TEST_CASE("http::get returns a response", "[http]") {
|
||||
// This test requires network, so we test error handling for invalid URL
|
||||
auto resp = http::get("http://0.0.0.0:1/nonexistent");
|
||||
// Should fail gracefully with status -1
|
||||
CHECK(resp.status_code == -1);
|
||||
CHECK(!resp.body.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("http::post returns a response", "[http]") {
|
||||
auto resp = http::post("http://0.0.0.0:1/nonexistent", R"({"test": true})");
|
||||
CHECK(resp.status_code == -1);
|
||||
CHECK(!resp.body.empty());
|
||||
}
|
||||
46
tests/unit/test_json.cpp
Normal file
46
tests/unit/test_json.cpp
Normal file
@ -0,0 +1,46 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "json/json.h"
|
||||
|
||||
TEST_CASE("json::is_valid accepts valid JSON", "[json]") {
|
||||
CHECK(json::is_valid(R"({"key": "value"})"));
|
||||
CHECK(json::is_valid("[]"));
|
||||
CHECK(json::is_valid("123"));
|
||||
CHECK(json::is_valid("\"hello\""));
|
||||
}
|
||||
|
||||
TEST_CASE("json::is_valid rejects invalid JSON", "[json]") {
|
||||
CHECK_FALSE(json::is_valid("{invalid}"));
|
||||
CHECK_FALSE(json::is_valid("{key: value}"));
|
||||
}
|
||||
|
||||
TEST_CASE("json::get_string extracts string values", "[json]") {
|
||||
auto val =
|
||||
json::get_string(R"({"name": "polymech", "version": "1.0"})", "name");
|
||||
CHECK(val == "polymech");
|
||||
}
|
||||
|
||||
TEST_CASE("json::get_string returns empty for missing key", "[json]") {
|
||||
auto val = json::get_string(R"({"name": "polymech"})", "missing");
|
||||
CHECK(val.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("json::get_int extracts int values", "[json]") {
|
||||
auto val = json::get_int(R"({"port": 8080, "name": "test"})", "port");
|
||||
CHECK(val == 8080);
|
||||
}
|
||||
|
||||
TEST_CASE("json::keys lists top-level keys", "[json]") {
|
||||
auto k = json::keys(R"({"a": 1, "b": 2, "c": 3})");
|
||||
REQUIRE(k.size() == 3);
|
||||
CHECK(k[0] == "a");
|
||||
CHECK(k[1] == "b");
|
||||
CHECK(k[2] == "c");
|
||||
}
|
||||
|
||||
TEST_CASE("json::prettify formats JSON", "[json]") {
|
||||
auto pretty = json::prettify(R"({"a":1})");
|
||||
REQUIRE(!pretty.empty());
|
||||
// Pretty output should contain newlines
|
||||
CHECK(pretty.find('\n') != std::string::npos);
|
||||
}
|
||||
22
tests/unit/test_logger.cpp
Normal file
22
tests/unit/test_logger.cpp
Normal file
@ -0,0 +1,22 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "logger/logger.h"
|
||||
|
||||
TEST_CASE("logger::init does not throw", "[logger]") {
|
||||
REQUIRE_NOTHROW(logger::init("test"));
|
||||
}
|
||||
|
||||
TEST_CASE("logger functions do not throw after init", "[logger]") {
|
||||
logger::init("test");
|
||||
|
||||
REQUIRE_NOTHROW(logger::info("info message"));
|
||||
REQUIRE_NOTHROW(logger::warn("warn message"));
|
||||
REQUIRE_NOTHROW(logger::error("error message"));
|
||||
REQUIRE_NOTHROW(logger::debug("debug message"));
|
||||
}
|
||||
|
||||
TEST_CASE("logger::init can be called multiple times", "[logger]") {
|
||||
REQUIRE_NOTHROW(logger::init("first"));
|
||||
REQUIRE_NOTHROW(logger::init("second"));
|
||||
REQUIRE_NOTHROW(logger::info("after re-init"));
|
||||
}
|
||||
14
tests/unit/test_postgres.cpp
Normal file
14
tests/unit/test_postgres.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "postgres/postgres.h"
|
||||
|
||||
TEST_CASE("postgres::connect returns ok for any input (stub)", "[postgres]") {
|
||||
auto result = postgres::connect("postgresql://localhost:5432/test");
|
||||
CHECK(result == "ok");
|
||||
}
|
||||
|
||||
TEST_CASE("postgres::connect accepts different connection strings",
|
||||
"[postgres]") {
|
||||
CHECK(postgres::connect("postgresql://user:pass@host:5432/db") == "ok");
|
||||
CHECK(postgres::connect("") == "ok");
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user