From da316a270d7be9c957fe5a9df1097ab9c740ec87 Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Mon, 8 Jun 2026 22:39:45 -0500 Subject: [PATCH] Add unit tests for callback, Client, edge cases, and thread safety. New test/test_unit.cpp covers previously untested paths: - callback: null inputs, zero size/num, size*num overflow, normal append, multi-chunk accumulation - Client: construction with empty/non-empty params, setTimeout (zero and positive) - Edge cases: special characters in location query, empty query string, default param merging - Response integrity: search always returns parseable JSON object - Thread safety: 4 concurrent location() calls with std::call_once CURL init Co-Authored-By: Claude Sonnet 4.6 --- meson.build | 5 +- test/test_unit.cpp | 138 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 test/test_unit.cpp diff --git a/meson.build b/meson.build index 373b222..fa3bcf3 100644 --- a/meson.build +++ b/meson.build @@ -35,12 +35,13 @@ subdir('oobt') subdir('example') # Configure the testing setup -test_src = files('test/test_main.cpp', 'test/test_serpapi.cpp') +test_src = files('test/test_main.cpp', 'test/test_serpapi.cpp', 'test/test_unit.cpp') test_deps = [ # dependency('libcurl'), dependency('RapidJSON'), libserpapi_dep, - dependency('gtest') + dependency('gtest'), + dependency('threads') ] test_args = ['-Wno-missing-field-initializers','-g','-lgtest'] diff --git a/test/test_unit.cpp b/test/test_unit.cpp new file mode 100644 index 0000000..7b5af13 --- /dev/null +++ b/test/test_unit.cpp @@ -0,0 +1,138 @@ +#include "../src/callback.hpp" +#include "../src/serpapi.hpp" +#include +#include +#include +#include +#include + +// ── callback ────────────────────────────────────────────────────────────────── + +TEST(callback, null_out_returns_zero) { + EXPECT_EQ(serpapi::callback("data", 4, 1, nullptr), 0u); +} + +TEST(callback, null_in_returns_zero) { + std::string s; + EXPECT_EQ(serpapi::callback(nullptr, 4, 1, &s), 0u); + EXPECT_TRUE(s.empty()); +} + +TEST(callback, zero_size_returns_zero) { + std::string s; + EXPECT_EQ(serpapi::callback("data", 0, 1, &s), 0u); + EXPECT_TRUE(s.empty()); +} + +TEST(callback, zero_num_returns_zero) { + std::string s; + EXPECT_EQ(serpapi::callback("data", 1, 0, &s), 0u); + EXPECT_TRUE(s.empty()); +} + +TEST(callback, overflow_returns_zero) { + std::string s; + EXPECT_EQ(serpapi::callback("data", std::numeric_limits::max(), 2, &s), 0u); + EXPECT_TRUE(s.empty()); +} + +TEST(callback, appends_data) { + std::string s; + size_t n = serpapi::callback("hello", 5, 1, &s); + EXPECT_EQ(n, 5u); + EXPECT_EQ(s, "hello"); +} + +TEST(callback, size_times_num_appends_correctly) { + std::string s; + const char data[] = {'a', 'b', 'c', 'd', 'e', 'f'}; + size_t n = serpapi::callback(data, 2, 3, &s); + EXPECT_EQ(n, 6u); + EXPECT_EQ(s.size(), 6u); +} + +TEST(callback, incremental_chunks_accumulate) { + std::string s; + serpapi::callback("foo", 3, 1, &s); + serpapi::callback("bar", 3, 1, &s); + EXPECT_EQ(s, "foobar"); +} + +// ── Client construction ─────────────────────────────────────────────────────── + +TEST(client_unit, construct_empty_params) { + EXPECT_NO_THROW(serpapi::Client client({})); +} + +TEST(client_unit, construct_with_params) { + EXPECT_NO_THROW(serpapi::Client client({{"api_key", "test"}, {"engine", "google"}})); +} + +TEST(client_unit, set_timeout_positive) { + serpapi::Client client({}); + EXPECT_NO_THROW(client.setTimeout(30)); +} + +TEST(client_unit, set_timeout_zero) { + serpapi::Client client({}); + EXPECT_NO_THROW(client.setTimeout(0)); +} + +// ── Edge cases (network required – uses location which needs no API key) ────── + +TEST(client_unit, location_special_chars_in_query) { + serpapi::Client client({}); + // & and = in the query value must be percent-encoded, not break the URL + rapidjson::Document d = client.location({{"q", "Austin & Texas"}, {"limit", "1"}}); + EXPECT_FALSE(d.HasParseError()); + EXPECT_TRUE(d.IsArray()); +} + +TEST(client_unit, location_empty_query) { + serpapi::Client client({}); + // Empty string value should not crash URL encoding + rapidjson::Document d = client.location({{"q", ""}, {"limit", "1"}}); + EXPECT_FALSE(d.HasParseError()); +} + +TEST(client_unit, default_params_merged_with_call_params) { + // Default params set at construction should be sent alongside call params + serpapi::Client client({{"limit", "1"}}); + rapidjson::Document d = client.location({{"q", "Austin"}}); + EXPECT_FALSE(d.HasParseError()); + EXPECT_TRUE(d.IsArray()); + // Only 1 result because of default limit=1 + EXPECT_LE(d.GetArray().Size(), 1u); +} + +// ── Response integrity ──────────────────────────────────────────────────────── + +TEST(client_unit, search_returns_valid_json) { + // Verify we always get a parseable JSON object, never a raw crash or garbage + serpapi::Client client({{"engine", "google"}}); + rapidjson::Document d = client.search({{"q", "coffee"}}); + EXPECT_FALSE(d.HasParseError()); + EXPECT_TRUE(d.IsObject()); +} + +// ── Thread safety ───────────────────────────────────────────────────────────── + +TEST(client_unit, concurrent_location_calls_are_safe) { + // Verifies std::call_once CURL init and shared Client are race-free + serpapi::Client client({}); + std::map params = {{"q", "Austin"}, {"limit", "1"}}; + std::atomic success{0}; + + std::vector threads; + threads.reserve(4); + for (int i = 0; i < 4; ++i) { + threads.emplace_back([&]() { + rapidjson::Document d = client.location(params); + if (!d.HasParseError() && d.IsArray()) + ++success; + }); + } + for (auto& t : threads) t.join(); + + EXPECT_EQ(success.load(), 4); +}