Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/client/http_client_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#pragma once

#include <string>
#include <httplib.h>

/**
* Interface for HTTP client implementations.
Expand All @@ -28,6 +29,11 @@
class HttpClientInterface {
public:
virtual ~HttpClientInterface() = default;

using HeaderList = httplib::Headers;

static constexpr const char* kJsonContentType = "application/json";
static constexpr const char* kDefaultUserAgent = "DBPSApiClient/1.0";

struct HttpResponse {
int status_code;
Expand All @@ -43,6 +49,21 @@ class HttpClientInterface {
: status_code(code), result(std::move(response_result)), error_message(std::move(error)) {}
};

static HeaderList DefaultJsonGetHeaders() {
HeaderList headers;
headers.insert({"Accept", kJsonContentType});
headers.insert({"User-Agent", kDefaultUserAgent});
return headers;
}

static HeaderList DefaultJsonPostHeaders() {
HeaderList headers;
headers.insert({"Content-Type", kJsonContentType});
headers.insert({"Accept", kJsonContentType});
headers.insert({"User-Agent", kDefaultUserAgent});
return headers;
}

virtual HttpResponse Get(const std::string& endpoint) = 0;
virtual HttpResponse Post(const std::string& endpoint, const std::string& json_body) = 0;
};
13 changes: 3 additions & 10 deletions src/client/httplib_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ HttpClientInterface::HttpResponse HttplibClient::Get(const std::string& endpoint
client.set_read_timeout(30);

// Set headers to indicate JSON responses
httplib::Headers headers = {
{"Accept", "application/json"},
{"User-Agent", "DBPSApiClient/1.0"}
};
auto headers = HttpClientInterface::DefaultJsonGetHeaders();

// Make the GET request
auto result = client.Get(endpoint, headers);
Expand All @@ -56,14 +53,10 @@ HttpClientInterface::HttpResponse HttplibClient::Post(const std::string& endpoin
client.set_read_timeout(30);

// Set headers for JSON content
httplib::Headers headers = {
{"Content-Type", "application/json"},
{"Accept", "application/json"},
{"User-Agent", "DBPSApiClient/1.0"}
};
auto headers = HttpClientInterface::DefaultJsonPostHeaders();

// Make the POST request
auto result = client.Post(endpoint, headers, json_body, "application/json");
auto result = client.Post(endpoint, headers, json_body, HttpClientInterface::kJsonContentType);

if (!result) {
return HttpResponse(0, "", "HTTP POST request failed: no response received");
Expand Down
17 changes: 3 additions & 14 deletions src/client/httplib_pooled_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,24 +170,13 @@ void HttplibPooledClient::WorkerLoop() {
auto perform_once = [&](RequestTask& t) -> std::pair<bool, HttpResponse> {
try {
if (t.kind == RequestTask::Kind::Get) {
//TODO: these are hardcoded and copied from httplib_client.cpp
// we should move them to a common place (header file).
httplib::Headers headers = {
{"Accept", "application/json"},
{"User-Agent", "DBPSApiClient/1.0"}
};
auto headers = HttpClientInterface::DefaultJsonGetHeaders();
auto res = client->Get(t.endpoint, headers);
if (!res) return {false, HttpResponse(0, "", "HTTP GET failed")};
return {true, HttpResponse(res->status, res->body)};
} else {
//TODO: these are hardcoded and copied from httplib_client.cpp
// we should move them to a common place (header file).
httplib::Headers headers = {
{"Content-Type", "application/json"},
{"Accept", "application/json"},
{"User-Agent", "DBPSApiClient/1.0"}
};
auto res = client->Post(t.endpoint, headers, t.json_body, "application/json");
auto headers = HttpClientInterface::DefaultJsonPostHeaders();
auto res = client->Post(t.endpoint, headers, t.json_body, HttpClientInterface::kJsonContentType);
if (!res) return {false, HttpResponse(0, "", "HTTP POST failed")};
return {true, HttpResponse(res->status, res->body)};
}
Expand Down
134 changes: 134 additions & 0 deletions src/common/json_request.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ std::optional<int> SafeParseToInt(const std::string& str) {
}
}

// Helper function to safely parse a string to 64-bit integer
std::optional<std::int64_t> SafeParseToInt64(const std::string& str) {
try {
std::size_t idx = 0;
long long value = std::stoll(str, &idx);
if (idx != str.size()) {
return std::nullopt;
}
return static_cast<std::int64_t>(value);
} catch (const std::exception&) {
return std::nullopt;
}
}

// Helper function to safely load JSON body and return nullopt if invalid or null
static std::optional<crow::json::rvalue> SafeLoadJsonBody(const std::string& json_string) {
auto json_body = crow::json::load(json_string);
Expand Down Expand Up @@ -129,6 +143,126 @@ std::string EncodeBase64Safe(const std::vector<uint8_t>& data) {
}
}

void TokenRequest::Parse(const std::string& request_body) {
client_id_.clear();
api_key_.clear();

auto json_body_opt = SafeLoadJsonBody(request_body);
if (!json_body_opt) {
return;
}

auto json_body = *json_body_opt;
if (json_body.t() != crow::json::type::Object) {
return;
}

if (auto parsed_value = SafeGetFromJsonPath(json_body, {"client_id"})) {
client_id_ = *parsed_value;
}

if (auto parsed_value = SafeGetFromJsonPath(json_body, {"api_key"})) {
api_key_ = *parsed_value;
}
}

std::string TokenRequest::ToJson() const {
crow::json::wvalue json;
if (!IsValid()) {
json["error"] = GetValidationError();
return PrettyPrintJson(json.dump());
}

json["client_id"] = client_id_;
json["api_key"] = api_key_;
return PrettyPrintJson(json.dump());
}

bool TokenRequest::IsValid() const {
return !client_id_.empty() && !api_key_.empty();
}

std::string TokenRequest::GetValidationError() const {
if (IsValid()) {
return "";
}
std::vector<std::string> missing_fields;
if (client_id_.empty()) missing_fields.push_back("client_id");
if (api_key_.empty()) missing_fields.push_back("api_key");
return BuildValidationError(missing_fields);
}

void TokenResponse::Parse(const std::string& response_body) {
token_ = std::nullopt;
expires_at_ = std::nullopt;
error_message_.clear();
error_status_code_ = 0;

auto json_body_opt = SafeLoadJsonBody(response_body);
if (!json_body_opt) {
return;
}

auto json_body = *json_body_opt;
if (json_body.t() != crow::json::type::Object) {
return;
}

if (auto parsed_value = SafeGetFromJsonPath(json_body, {"error"})) {
error_message_ = *parsed_value;
}

if (auto parsed_value = SafeGetFromJsonPath(json_body, {"token"})) {
token_ = *parsed_value;
}

if (auto parsed_value = SafeGetFromJsonPath(json_body, {"expires_at"})) {
expires_at_ = SafeParseToInt64(*parsed_value);
}
}

bool TokenResponse::IsValid() const {
return error_message_.empty() &&
token_.has_value() &&
!token_.value().empty() &&
expires_at_.has_value();
}

std::string TokenResponse::GetValidationError() const {
if (IsValid()) {
return "";
}
if (!error_message_.empty()) {
return error_message_;
}
std::vector<std::string> missing_fields;
if (!token_.has_value() || token_.value().empty()) missing_fields.push_back("token");
if (!expires_at_.has_value()) missing_fields.push_back("expires_at");
return BuildValidationError(missing_fields);
}

void TokenResponse::SetErrorStatusCodeAndClearToken(int status_code) {
token_ = std::nullopt;
expires_at_ = std::nullopt;
error_message_.clear();
error_status_code_ = status_code;
}

std::string TokenResponse::ToJson() const {
crow::json::wvalue json;

if (!IsValid()) {
json["error"] = GetValidationError();
return json.dump();
}

json["token"] = token_.value();
json["token_type"] = "Bearer";
json["expires_at"] = expires_at_.value();

return PrettyPrintJson(json.dump());
}

// JsonRequest implementation
void JsonRequest::ParseCommon(const std::string& request_body) {
// Load and validate JSON first
Expand Down
36 changes: 36 additions & 0 deletions src/common/json_request.h
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,39 @@ class DecryptJsonResponse : public JsonResponse {
*/
std::string ToJsonString() const override;
};

/**
* Structure to hold parsed token request data.
*/
class TokenRequest {
public:
TokenRequest() = default;

std::string client_id_;
std::string api_key_;

void Parse(const std::string& request_body);
bool IsValid() const;
std::string GetValidationError() const;
std::string ToJson() const;
};

/**
* Structure to hold token generation result.
*/
class TokenResponse {
public:
TokenResponse() = default;

std::optional<std::string> token_;
std::optional<std::int64_t> expires_at_;
std::string error_message_;
int error_status_code_ = 0;

void Parse(const std::string& response_body);
bool IsValid() const;
std::string GetValidationError() const;
std::string ToJson() const;

void SetErrorStatusCodeAndClearToken(int status_code);
};
93 changes: 93 additions & 0 deletions src/common/json_request_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ std::string BinaryToString(const std::vector<uint8_t>& binary_data) {

// Forward declarations for internal functions from json_request.cpp
std::optional<std::string> SafeGetFromJsonPath(const crow::json::rvalue& json_body, const std::vector<std::string>& path);
std::optional<std::int64_t> SafeParseToInt64(const std::string& str);
std::optional<int> SafeParseToInt(const std::string& str);

// Test-specific derived class to access protected methods
Expand Down Expand Up @@ -442,6 +443,24 @@ TEST(JsonRequest, SafeGetFromJsonPathInvalidPath) {
ASSERT_FALSE(result.has_value());
}

TEST(JsonRequest, SafeParseToInt64StrictParsing) {
// Valid numeric string
auto v1 = SafeParseToInt64("123");
ASSERT_TRUE(v1.has_value());
ASSERT_EQ(v1.value(), 123);

// Typical epoch-seconds value
auto epoch = SafeParseToInt64("1766138275");
ASSERT_TRUE(epoch.has_value());
ASSERT_EQ(epoch.value(), 1766138275);

// Invalid: numeric prefix + junk
ASSERT_FALSE(SafeParseToInt64("123abc").has_value());

// Invalid: trailing whitespace
ASSERT_FALSE(SafeParseToInt64("123 ").has_value());
}

// Test JSON generation functionality
TEST(JsonRequest, EncryptJsonRequestToJson) {
EncryptJsonRequest request;
Expand Down Expand Up @@ -940,3 +959,77 @@ TEST(JsonRequest, DecryptJsonResponseInvalidDatatypeLength) {
std::string error = response.GetValidationError();
ASSERT_TRUE(error.find("data_batch.datatype_info.length (invalid integer value)") != std::string::npos);
}

// Token request and response tests (simplified)
TEST(JsonRequest, TokenRequestParse) {
{
TokenRequest req;
req.Parse(R"({"client_id":"client1","api_key":"key1"})");
ASSERT_TRUE(req.IsValid());
ASSERT_EQ(req.client_id_, "client1");
ASSERT_EQ(req.api_key_, "key1");
}
{
TokenRequest req;
req.Parse(R"({"api_key":"key1"})");
ASSERT_FALSE(req.IsValid());
ASSERT_FALSE(req.GetValidationError().empty());
}
{
TokenRequest req;
req.Parse("{ invalid json }");
ASSERT_FALSE(req.IsValid());
ASSERT_FALSE(req.GetValidationError().empty());
}
}

TEST(JsonRequest, TokenRequestToJson) {
TokenRequest req;
req.Parse(R"({"client_id":"clientA","api_key":"keyA"})");
ASSERT_TRUE(req.IsValid());

auto json = crow::json::load(req.ToJson());
ASSERT_TRUE(json);
ASSERT_EQ(std::string(json["client_id"]), "clientA");
ASSERT_EQ(std::string(json["api_key"]), "keyA");
}

TEST(JsonRequest, TokenResponseRoundTrip) {
TokenResponse resp;
resp.Parse(R"({"token":"token123","expires_at":1234567890})");
ASSERT_TRUE(resp.IsValid());

auto json = crow::json::load(resp.ToJson());
ASSERT_TRUE(json);
ASSERT_EQ(std::string(json["token"]), "token123");
ASSERT_EQ(std::string(json["token_type"]), "Bearer");

auto expires_at_str = SafeGetFromJsonPath(json, {"expires_at"});
ASSERT_TRUE(expires_at_str.has_value());
ASSERT_EQ(std::stoll(*expires_at_str), 1234567890);

TokenResponse parsed;
parsed.Parse(resp.ToJson());
ASSERT_TRUE(parsed.IsValid());
ASSERT_EQ(parsed.token_.value(), "token123");
ASSERT_EQ(parsed.expires_at_.value(), 1234567890);
}

TEST(JsonRequest, TokenResponseParseError) {
TokenResponse resp;
resp.Parse(R"({"error":"unauthorized"})");
ASSERT_FALSE(resp.IsValid());
ASSERT_FALSE(resp.GetValidationError().empty());
}

TEST(JsonRequest, TokenResponseSetErrorStatusCodeAndClearToken) {
TokenResponse resp;
resp.Parse(R"({"token":"token123","expires_at":1234567890})");
ASSERT_TRUE(resp.IsValid());

resp.SetErrorStatusCodeAndClearToken(401);
ASSERT_FALSE(resp.IsValid());
ASSERT_FALSE(resp.token_.has_value());
ASSERT_FALSE(resp.expires_at_.has_value());
ASSERT_EQ(resp.error_status_code_, 401);
}
Loading