diff --git a/include/aws/http/response.h b/include/aws/http/response.h index 3740c22..f34659d 100644 --- a/include/aws/http/response.h +++ b/include/aws/http/response.h @@ -34,7 +34,7 @@ class response { inline void append_body(const char* p, size_t sz); inline bool has_header(char const* header) const; inline lambda_runtime::outcome get_header(char const* header) const; - inline response_code get_response_code() const { return m_response_code; } + response_code get_response_code() const { return m_response_code; } inline void set_response_code(aws::http::response_code c); inline void set_content_type(char const* ct); inline std::string const& get_body() const; diff --git a/include/aws/lambda-runtime/runtime.h b/include/aws/lambda-runtime/runtime.h index be3961c..44244ad 100644 --- a/include/aws/lambda-runtime/runtime.h +++ b/include/aws/lambda-runtime/runtime.h @@ -167,14 +167,13 @@ class runtime { private: void set_curl_next_options(); - void set_curl_post_result_options(); + static void set_curl_post_result_options(); post_outcome do_post( std::string const& url, std::string const& request_id, invocation_response const& handler_response); std::string const m_user_agent_header; std::array const m_endpoints; - CURL* const m_curl_handle; }; inline std::chrono::milliseconds invocation_request::get_time_remaining() const diff --git a/include/aws/logging/logging.h b/include/aws/logging/logging.h index 0b5d0ef..31e1308 100644 --- a/include/aws/logging/logging.h +++ b/include/aws/logging/logging.h @@ -15,11 +15,12 @@ */ #include +#include namespace aws { namespace logging { -enum class verbosity { +enum class verbosity : std::uint8_t { error, info, debug, diff --git a/src/logging.cpp b/src/logging.cpp index e68aafa..d04bb5d 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -14,6 +14,7 @@ */ #include "aws/logging/logging.h" #include +#include #include #include @@ -21,8 +22,9 @@ namespace aws { namespace logging { +namespace { -static inline char const* get_prefix(verbosity v) +inline char const* get_prefix(verbosity v) { switch (v) { case verbosity::error: @@ -36,6 +38,8 @@ static inline char const* get_prefix(verbosity v) } } +} // namespace + LAMBDA_RUNTIME_API void log(verbosity v, char const* tag, char const* msg, va_list args) { diff --git a/src/runtime.cpp b/src/runtime.cpp index efbb697..0bdf472 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -42,6 +42,7 @@ static constexpr auto COGNITO_IDENTITY_HEADER = "lambda-runtime-cognito-identity static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms"; static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn"; static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id"; +thread_local static CURL* m_curl_handle = curl_easy_init(); enum Endpoints { INIT, @@ -155,7 +156,7 @@ static int rt_curl_debug_callback(CURL* handle, curl_infotype type, char* data, (void)handle; (void)type; (void)userdata; - std::string s(data, size); + const std::string s(data, size); logging::log_debug(LOG_TAG, "CURL DBG: %s", s.c_str()); return 0; } @@ -164,67 +165,62 @@ static int rt_curl_debug_callback(CURL* handle, curl_infotype type, char* data, runtime::runtime(std::string const& endpoint) : runtime(endpoint, "AWS_Lambda_Cpp/" + std::string(get_version())) {} runtime::runtime(std::string const& endpoint, std::string const& user_agent) - : m_user_agent_header("User-Agent: " + user_agent), - m_endpoints{ - {endpoint + "/2018-06-01/runtime/init/error", - endpoint + "/2018-06-01/runtime/invocation/next", - endpoint + "/2018-06-01/runtime/invocation/"}}, - m_curl_handle(curl_easy_init()) + : m_user_agent_header("User-Agent: " + user_agent), m_endpoints{ + {endpoint + "/2018-06-01/runtime/init/error", + endpoint + "/2018-06-01/runtime/invocation/next", + endpoint + "/2018-06-01/runtime/invocation/"}} { - if (!m_curl_handle) { + if (!lambda_runtime::m_curl_handle) { logging::log_error(LOG_TAG, "Failed to acquire curl easy handle for next."); } } -runtime::~runtime() -{ - curl_easy_cleanup(m_curl_handle); -} +runtime::~runtime() = default; void runtime::set_curl_next_options() { // lambda freezes the container when no further tasks are available. The freezing period could be longer than the // request timeout, which causes the following get_next request to fail with a timeout error. - curl_easy_reset(m_curl_handle); - curl_easy_setopt(m_curl_handle, CURLOPT_TIMEOUT, 0L); - curl_easy_setopt(m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_TCP_NODELAY, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_reset(lambda_runtime::m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TIMEOUT, 0L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TCP_NODELAY, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_URL, m_endpoints[Endpoints::NEXT].c_str()); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_URL, m_endpoints[Endpoints::NEXT].c_str()); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); - curl_easy_setopt(m_curl_handle, CURLOPT_PROXY, ""); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_PROXY, ""); #ifndef NDEBUG - curl_easy_setopt(m_curl_handle, CURLOPT_VERBOSE, 1); - curl_easy_setopt(m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_VERBOSE, 1); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); #endif } void runtime::set_curl_post_result_options() { - curl_easy_reset(m_curl_handle); - curl_easy_setopt(m_curl_handle, CURLOPT_TIMEOUT, 0L); - curl_easy_setopt(m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_TCP_NODELAY, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_reset(lambda_runtime::m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TIMEOUT, 0L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TCP_NODELAY, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_easy_setopt(m_curl_handle, CURLOPT_POST, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_READFUNCTION, read_data); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_POST, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_READFUNCTION, read_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); - curl_easy_setopt(m_curl_handle, CURLOPT_PROXY, ""); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_PROXY, ""); #ifndef NDEBUG - curl_easy_setopt(m_curl_handle, CURLOPT_VERBOSE, 1); - curl_easy_setopt(m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_VERBOSE, 1); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); #endif } @@ -232,15 +228,15 @@ runtime::next_outcome runtime::get_next() { http::response resp; set_curl_next_options(); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERDATA, &resp); curl_slist* headers = nullptr; headers = curl_slist_append(headers, m_user_agent_header.c_str()); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); logging::log_debug(LOG_TAG, "Making request to %s", m_endpoints[Endpoints::NEXT].c_str()); - CURLcode curl_code = curl_easy_perform(m_curl_handle); + const CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); logging::log_debug(LOG_TAG, "Completed request to %s", m_endpoints[Endpoints::NEXT].c_str()); curl_slist_free_all(headers); @@ -255,13 +251,13 @@ runtime::next_outcome runtime::get_next() { long resp_code; - curl_easy_getinfo(m_curl_handle, CURLINFO_RESPONSE_CODE, &resp_code); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_RESPONSE_CODE, &resp_code); resp.set_response_code(static_cast(resp_code)); } { char* content_type = nullptr; - curl_easy_getinfo(m_curl_handle, CURLINFO_CONTENT_TYPE, &content_type); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_CONTENT_TYPE, &content_type); resp.set_content_type(content_type); } @@ -312,7 +308,7 @@ runtime::next_outcome runtime::get_next() if (out.is_success()) { auto const& deadline_string = std::move(out).get_result(); constexpr int base = 10; - unsigned long ms = strtoul(deadline_string.c_str(), nullptr, base); + const unsigned long ms = strtoul(deadline_string.c_str(), nullptr, base); assert(ms > 0); assert(ms < ULONG_MAX); req.deadline += std::chrono::milliseconds(ms); @@ -343,7 +339,7 @@ runtime::post_outcome runtime::do_post( invocation_response const& handler_response) { set_curl_post_result_options(); - curl_easy_setopt(m_curl_handle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_URL, url.c_str()); logging::log_info(LOG_TAG, "Making request to %s", url.c_str()); curl_slist* headers = nullptr; @@ -364,11 +360,11 @@ runtime::post_outcome runtime::do_post( std::pair ctx{payload, 0}; aws::http::response resp; - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_READDATA, &ctx); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPHEADER, headers); - CURLcode curl_code = curl_easy_perform(m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_READDATA, &ctx); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); + const CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); curl_slist_free_all(headers); if (curl_code != CURLE_OK) { @@ -382,7 +378,7 @@ runtime::post_outcome runtime::do_post( } long http_response_code; - curl_easy_getinfo(m_curl_handle, CURLINFO_RESPONSE_CODE, &http_response_code); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_RESPONSE_CODE, &http_response_code); if (!is_success(aws::http::response_code(http_response_code))) { logging::log_error( @@ -448,7 +444,7 @@ void run_handler(std::function c auto const req = std::move(next_outcome).get_result(); logging::log_info(LOG_TAG, "Invoking user handler"); - invocation_response res = handler(req); + const invocation_response res = handler(req); logging::log_info(LOG_TAG, "Invoking user handler completed."); if (res.is_success()) { @@ -476,7 +472,7 @@ static std::string json_escape(std::string const& in) constexpr char last_non_printable_character = 31; std::string out; out.reserve(in.length()); // most strings will end up identical - for (char ch : in) { + for (const char ch : in) { if (ch > last_non_printable_character && ch != '\"' && ch != '\\') { out.append(1, ch); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7406096..2774aa2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,8 @@ if(DEFINED ENV{GITHUB_ACTIONS}) FetchContent_MakeAvailable(gtest) add_executable(unit_tests - unit/no_op_test.cpp) + unit/thread_local_curl_test.cpp + unit/unit_tests.cpp) target_link_libraries(unit_tests PRIVATE gtest_main aws-lambda-runtime) # Register unit tests @@ -25,7 +26,22 @@ if(DEFINED ENV{GITHUB_ACTIONS}) LABELS "unit" DISCOVERY_TIMEOUT 10) else() - message(STATUS "Unit tests skipped: Not in GitHub Actions environment") + # Build unit tests using the bundled gtest + add_executable(unit_tests + unit/thread_local_curl_test.cpp + unit/unit_tests.cpp + gtest/gtest-all.cc) + target_include_directories(unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(unit_tests PRIVATE aws-lambda-runtime pthread) + + # Provide a main() for gtest + target_sources(unit_tests PRIVATE unit/main.cpp) + + include(GoogleTest) + gtest_discover_tests(unit_tests + PROPERTIES + LABELS "unit" + DISCOVERY_TIMEOUT 10) endif() diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp new file mode 100644 index 0000000..08fb839 --- /dev/null +++ b/tests/unit/main.cpp @@ -0,0 +1,7 @@ +#include "gtest/gtest.h" + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/unit/no_op_test.cpp b/tests/unit/no_op_test.cpp deleted file mode 100644 index c9a3b7d..0000000 --- a/tests/unit/no_op_test.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include - -TEST(noop, dummy_test) -{ - ASSERT_EQ(0, 0); -} diff --git a/tests/unit/thread_local_curl_test.cpp b/tests/unit/thread_local_curl_test.cpp new file mode 100644 index 0000000..bf0f6e6 --- /dev/null +++ b/tests/unit/thread_local_curl_test.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include + +using namespace aws::lambda_runtime; + +TEST(ThreadLocalCurl, runtime_construction_succeeds) +{ + runtime rt("http://127.0.0.1:9001"); + auto outcome = rt.get_next(); + // We expect a connection failure since there's no server, but the runtime itself should construct fine + ASSERT_FALSE(outcome.is_success()); +} + +TEST(ThreadLocalCurl, multiple_runtimes_on_same_thread_do_not_crash) +{ + { + runtime rt1("http://127.0.0.1:9001"); + auto o1 = rt1.get_next(); + ASSERT_FALSE(o1.is_success()); + } + { + runtime rt2("http://127.0.0.1:9001"); + auto o2 = rt2.get_next(); + ASSERT_FALSE(o2.is_success()); + } +} + +TEST(ThreadLocalCurl, concurrent_runtimes_on_different_threads) +{ + constexpr int num_threads = 4; + std::vector threads; + std::atomic successes{0}; + + for (int i = 0; i < num_threads; ++i) { + threads.emplace_back([&successes]() { + runtime rt("http://127.0.0.1:9001"); + auto outcome = rt.get_next(); + // Connection will fail but runtime should not crash or corrupt state + if (!outcome.is_success()) { + successes.fetch_add(1); + } + }); + } + + for (auto& t : threads) { + t.join(); + } + + ASSERT_EQ(successes.load(), num_threads); +} + +TEST(ThreadLocalCurl, sequential_requests_on_same_runtime) +{ + runtime rt("http://127.0.0.1:9001"); + for (int i = 0; i < 5; ++i) { + auto outcome = rt.get_next(); + ASSERT_FALSE(outcome.is_success()); + } +} diff --git a/tests/unit/unit_tests.cpp b/tests/unit/unit_tests.cpp new file mode 100644 index 0000000..acfef45 --- /dev/null +++ b/tests/unit/unit_tests.cpp @@ -0,0 +1,256 @@ +#include +#include +#include +#include +#include "gtest/gtest.h" + +using namespace aws::lambda_runtime; +using namespace aws::http; + +// --- invocation_response tests --- + +TEST(InvocationResponseTest, success_response_has_correct_payload_and_content_type) +{ + auto resp = invocation_response::success("hello world", "text/plain"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ("hello world", resp.get_payload()); + EXPECT_EQ("text/plain", resp.get_content_type()); +} + +TEST(InvocationResponseTest, success_response_with_json) +{ + auto resp = invocation_response::success(R"({"key":"value"})", "application/json"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ(R"({"key":"value"})", resp.get_payload()); + EXPECT_EQ("application/json", resp.get_content_type()); +} + +TEST(InvocationResponseTest, success_response_with_empty_payload) +{ + auto resp = invocation_response::success("", "application/json"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ("", resp.get_payload()); +} + +TEST(InvocationResponseTest, failure_response_is_not_success) +{ + auto resp = invocation_response::failure("something broke", "RuntimeError"); + EXPECT_FALSE(resp.is_success()); + EXPECT_EQ("application/json", resp.get_content_type()); +} + +TEST(InvocationResponseTest, failure_response_contains_error_message) +{ + auto resp = invocation_response::failure("something broke", "RuntimeError"); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find("something broke")); + EXPECT_NE(std::string::npos, payload.find("RuntimeError")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_quotes) +{ + auto resp = invocation_response::failure(R"(error with "quotes")", "TestError"); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(error with \"quotes\")")); + EXPECT_EQ(std::string::npos, payload.find(R"(error with "quotes")")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_backslash) +{ + auto resp = invocation_response::failure(R"(path\to\file)", "TestError"); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(path\\to\\file)")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_newlines) +{ + auto resp = invocation_response::failure("line1\nline2\r\n", "TestError"); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(line1\nline2\r\n)")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_tabs) +{ + auto resp = invocation_response::failure("col1\tcol2", "TestError"); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find(R"(col1\tcol2)")); +} + +TEST(InvocationResponseTest, failure_response_json_escapes_control_characters) +{ + std::string msg = "null\x00 byte"; + msg.push_back('\x01'); + auto resp = invocation_response::failure(msg, "TestError"); + auto const& payload = resp.get_payload(); + EXPECT_NE(std::string::npos, payload.find("\\u0001")); +} + +TEST(InvocationResponseTest, success_response_with_binary_content_type) +{ + std::string binary_data(256, '\0'); + for (int i = 0; i < 256; ++i) { + binary_data[static_cast(i)] = static_cast(i); + } + auto resp = invocation_response::success(binary_data, "application/octet-stream"); + EXPECT_TRUE(resp.is_success()); + EXPECT_EQ(256u, resp.get_payload().size()); +} + +TEST(InvocationResponseTest, constructor_based_failure) +{ + auto resp = invocation_response(R"({"custom":"error"})", "application/json", false); + EXPECT_FALSE(resp.is_success()); + EXPECT_EQ(R"({"custom":"error"})", resp.get_payload()); +} + +// --- http::response tests --- + +TEST(HttpResponseTest, add_and_retrieve_header) +{ + response resp; + resp.add_header("Content-Type", "application/json"); + EXPECT_TRUE(resp.has_header("content-type")); + auto header = resp.get_header("content-type"); + EXPECT_TRUE(header.is_success()); + EXPECT_EQ("application/json", header.get_result()); +} + +TEST(HttpResponseTest, headers_are_lowercased) +{ + response resp; + resp.add_header("X-Custom-Header", "some-value"); + EXPECT_TRUE(resp.has_header("x-custom-header")); + EXPECT_FALSE(resp.has_header("X-Custom-Header")); +} + +TEST(HttpResponseTest, has_header_returns_false_for_missing) +{ + response resp; + resp.add_header("Content-Type", "text/plain"); + EXPECT_FALSE(resp.has_header("x-missing")); +} + +TEST(HttpResponseTest, append_body_accumulates) +{ + response resp; + resp.append_body("hello", 5); + resp.append_body(" world", 6); + EXPECT_EQ("hello world", resp.get_body()); +} + +TEST(HttpResponseTest, append_body_empty) +{ + response resp; + EXPECT_EQ("", resp.get_body()); +} + +TEST(HttpResponseTest, set_response_code) +{ + response resp; + resp.set_response_code(response_code::OK); + EXPECT_EQ(response_code::OK, resp.get_response_code()); +} + +TEST(HttpResponseTest, multiple_headers) +{ + response resp; + resp.add_header("lambda-runtime-aws-request-id", "req-123"); + resp.add_header("lambda-runtime-trace-id", "trace-456"); + resp.add_header("lambda-runtime-deadline-ms", "1234567890"); + EXPECT_EQ("req-123", resp.get_header("lambda-runtime-aws-request-id").get_result()); + EXPECT_EQ("trace-456", resp.get_header("lambda-runtime-trace-id").get_result()); + EXPECT_EQ("1234567890", resp.get_header("lambda-runtime-deadline-ms").get_result()); +} + +// --- outcome tests --- + +TEST(OutcomeTest, success_outcome) +{ + outcome o(std::string("result")); + EXPECT_TRUE(o.is_success()); + EXPECT_EQ("result", o.get_result()); +} + +TEST(OutcomeTest, failure_outcome) +{ + outcome o(42); + EXPECT_FALSE(o.is_success()); + EXPECT_EQ(42, o.get_failure()); +} + +TEST(OutcomeTest, move_success) +{ + outcome o1(std::string("moved")); + outcome o2(std::move(o1)); + EXPECT_TRUE(o2.is_success()); + EXPECT_EQ("moved", o2.get_result()); +} + +TEST(OutcomeTest, move_failure) +{ + outcome o1(99); + outcome o2(std::move(o1)); + EXPECT_FALSE(o2.is_success()); + EXPECT_EQ(99, o2.get_failure()); +} + +TEST(OutcomeTest, with_response_code) +{ + using test_outcome = outcome; + test_outcome success(no_result{}); + EXPECT_TRUE(success.is_success()); + + test_outcome failure(response_code::INTERNAL_SERVER_ERROR); + EXPECT_FALSE(failure.is_success()); + EXPECT_EQ(response_code::INTERNAL_SERVER_ERROR, failure.get_failure()); +} + +// --- invocation_request tests --- + +TEST(InvocationRequestTest, get_time_remaining_future_deadline) +{ + invocation_request req; + req.deadline = std::chrono::system_clock::now() + std::chrono::seconds(30); + auto remaining = req.get_time_remaining(); + EXPECT_GT(remaining.count(), 29000); + EXPECT_LE(remaining.count(), 30000); +} + +TEST(InvocationRequestTest, get_time_remaining_past_deadline) +{ + invocation_request req; + req.deadline = std::chrono::system_clock::now() - std::chrono::seconds(5); + auto remaining = req.get_time_remaining(); + EXPECT_LT(remaining.count(), 0); +} + +TEST(InvocationRequestTest, default_fields_are_empty) +{ + invocation_request req; + EXPECT_TRUE(req.payload.empty()); + EXPECT_TRUE(req.request_id.empty()); + EXPECT_TRUE(req.xray_trace_id.empty()); + EXPECT_TRUE(req.client_context.empty()); + EXPECT_TRUE(req.cognito_identity.empty()); + EXPECT_TRUE(req.function_arn.empty()); + EXPECT_TRUE(req.tenant_id.empty()); +} + +// --- version tests (no AWS SDK needed) --- + +TEST(VersionTest, version_string_not_empty) +{ + EXPECT_NE(nullptr, get_version()); + EXPECT_GT(strlen(get_version()), 0u); +} + +TEST(VersionTest, version_format) +{ + std::string v = get_version(); + int dots = 0; + for (char c : v) { + if (c == '.') + dots++; + } + EXPECT_EQ(2, dots); +}