Skip to content
Draft
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
122 changes: 49 additions & 73 deletions contract-tests/server-contract-tests/src/contract_test_hook.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#include "contract_test_hook.hpp"

#include <launchdarkly/detail/serialization/json_primitives.hpp>
#include <launchdarkly/detail/serialization/json_value.hpp>
#include <launchdarkly/serialization/json_context.hpp>
#include <launchdarkly/serialization/json_evaluation_reason.hpp>
#include <launchdarkly/detail/serialization/json_value.hpp>
#include <launchdarkly/detail/serialization/json_primitives.hpp>

#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
Expand Down Expand Up @@ -55,10 +55,8 @@ std::optional<std::string> ContractTestHook::GetErrorForStage(

void ContractTestHook::PostCallback(std::string const& stage,
nlohmann::json const& payload) {
// Parse the callback URI
auto uri_result = boost::urls::parse_uri(config_.callbackUri);
if (!uri_result) {
// Invalid URI, skip callback
return;
}

Expand All @@ -70,57 +68,44 @@ void ContractTestHook::PostCallback(std::string const& stage,
target = "/";
}

std::string body = payload.dump();

// Create a shared connection and post asynchronously without blocking
// This uses fire-and-forget semantics - we don't wait for response
auto conn = std::make_shared<beast::tcp_stream>(executor_);
auto resolver = std::make_shared<tcp::resolver>(executor_);

// Capture everything needed by value in shared_ptrs
auto req = std::make_shared<http::request<http::string_body>>();
req->method(http::verb::post);
req->target(target);
req->version(11);
req->set(http::field::host, host);
req->set(http::field::user_agent, "cpp-server-sdk-contract-tests");
req->set(http::field::content_type, "application/json");
req->body() = body;
req->prepare_payload();

// Start async resolution
resolver->async_resolve(host, port,
[conn, resolver, req](beast::error_code ec, tcp::resolver::results_type results) {
if (ec) return; // Silently ignore resolution errors

// Connect asynchronously
conn->async_connect(results,
[conn, resolver, req](beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
if (ec) return; // Silently ignore connection errors

// Write request asynchronously
http::async_write(*conn, *req,
[conn, resolver, req](beast::error_code ec, std::size_t) {
if (ec) return; // Silently ignore write errors

// Read response asynchronously (but don't wait for result)
auto res = std::make_shared<http::response<http::string_body>>();
auto buffer = std::make_shared<beast::flat_buffer>();
http::async_read(*conn, *buffer, *res,
[conn, resolver, req, res, buffer](beast::error_code ec, std::size_t) {
// Close connection gracefully
conn->socket().shutdown(tcp::socket::shutdown_both, ec);
// Cleanup happens automatically via shared_ptr
});
});
});
});
// Send synchronously so callbacks from sequential hook invocations arrive
// at the harness in the order the SDK invoked them.
beast::error_code ec;
tcp::resolver resolver(executor_);
auto results = resolver.resolve(host, port, ec);
if (ec) {
return;
}

beast::tcp_stream stream(executor_);
stream.connect(results, ec);
if (ec) {
return;
}

http::request<http::string_body> req{http::verb::post, target, 11};
req.set(http::field::host, host);
req.set(http::field::user_agent, "cpp-server-sdk-contract-tests");
req.set(http::field::content_type, "application/json");
req.body() = payload.dump();
req.prepare_payload();

http::write(stream, req, ec);
if (ec) {
return;
}

beast::flat_buffer buffer;
http::response<http::string_body> res;
http::read(stream, buffer, res, ec);

beast::error_code ignore_ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ignore_ec);
}

EvaluationSeriesData ContractTestHook::BeforeEvaluation(
EvaluationSeriesContext const& series_context,
EvaluationSeriesData data) {

// Check if we should throw an error for this stage
auto error = GetErrorForStage("beforeEvaluation");
if (error) {
Expand Down Expand Up @@ -149,12 +134,10 @@ EvaluationSeriesData ContractTestHook::BeforeEvaluation(
// EvaluationSeriesContext
nlohmann::json ctx;
ctx["flagKey"] = std::string(series_context.FlagKey());
ctx["context"] = nlohmann::json::parse(
boost::json::serialize(
boost::json::value_from(series_context.EvaluationContext())));
ctx["defaultValue"] = nlohmann::json::parse(
boost::json::serialize(
boost::json::value_from(series_context.DefaultValue())));
ctx["context"] = nlohmann::json::parse(boost::json::serialize(
boost::json::value_from(series_context.EvaluationContext())));
ctx["defaultValue"] = nlohmann::json::parse(boost::json::serialize(
boost::json::value_from(series_context.DefaultValue())));
ctx["method"] = std::string(series_context.Method());
if (series_context.EnvironmentId()) {
ctx["environmentId"] = std::string(*series_context.EnvironmentId());
Expand All @@ -181,7 +164,6 @@ EvaluationSeriesData ContractTestHook::AfterEvaluation(
EvaluationSeriesContext const& series_context,
EvaluationSeriesData data,
launchdarkly::EvaluationDetail<launchdarkly::Value> const& detail) {

// Check if we should throw an error for this stage
auto error = GetErrorForStage("afterEvaluation");
if (error) {
Expand All @@ -195,12 +177,10 @@ EvaluationSeriesData ContractTestHook::AfterEvaluation(
// EvaluationSeriesContext
nlohmann::json ctx;
ctx["flagKey"] = std::string(series_context.FlagKey());
ctx["context"] = nlohmann::json::parse(
boost::json::serialize(
boost::json::value_from(series_context.EvaluationContext())));
ctx["defaultValue"] = nlohmann::json::parse(
boost::json::serialize(
boost::json::value_from(series_context.DefaultValue())));
ctx["context"] = nlohmann::json::parse(boost::json::serialize(
boost::json::value_from(series_context.EvaluationContext())));
ctx["defaultValue"] = nlohmann::json::parse(boost::json::serialize(
boost::json::value_from(series_context.DefaultValue())));
ctx["method"] = std::string(series_context.Method());
if (series_context.EnvironmentId()) {
ctx["environmentId"] = std::string(*series_context.EnvironmentId());
Expand Down Expand Up @@ -236,9 +216,7 @@ EvaluationSeriesData ContractTestHook::AfterEvaluation(
return data;
}

void ContractTestHook::AfterTrack(
TrackSeriesContext const& series_context) {

void ContractTestHook::AfterTrack(TrackSeriesContext const& series_context) {
// Check if we should throw an error for this stage
auto error = GetErrorForStage("afterTrack");
if (error) {
Expand All @@ -251,17 +229,15 @@ void ContractTestHook::AfterTrack(

// TrackSeriesContext
nlohmann::json ctx;
ctx["context"] = nlohmann::json::parse(
boost::json::serialize(
boost::json::value_from(series_context.TrackContext())));
ctx["context"] = nlohmann::json::parse(boost::json::serialize(
boost::json::value_from(series_context.TrackContext())));
ctx["key"] = std::string(series_context.Key());
if (series_context.MetricValue()) {
ctx["metricValue"] = *series_context.MetricValue();
}
if (series_context.Data()) {
ctx["data"] = nlohmann::json::parse(
boost::json::serialize(
boost::json::value_from(series_context.Data()->get())));
ctx["data"] = nlohmann::json::parse(boost::json::serialize(
boost::json::value_from(series_context.Data()->get())));
}
if (series_context.EnvironmentId()) {
ctx["environmentId"] = std::string(*series_context.EnvironmentId());
Expand Down
Loading