Handle percent encoding
This commit is contained in:
@@ -1,117 +1,554 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "api_url_parser.hpp"
|
||||
|
||||
// Helper to convert string to mutable buffer for testing
|
||||
std::string make_mutable_copy(const std::string &url) {
|
||||
return url; // Return copy that can be modified
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser routing") {
|
||||
SUBCASE("Static GET routes") {
|
||||
auto match = ApiUrlParser::parse("GET", "/v1/version");
|
||||
auto url = make_mutable_copy("/v1/version");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetVersion);
|
||||
|
||||
match = ApiUrlParser::parse("GET", "/v1/subscribe");
|
||||
url = make_mutable_copy("/v1/subscribe");
|
||||
result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetSubscribe);
|
||||
|
||||
match = ApiUrlParser::parse("GET", "/metrics");
|
||||
url = make_mutable_copy("/metrics");
|
||||
result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetMetrics);
|
||||
|
||||
match = ApiUrlParser::parse("GET", "/ok");
|
||||
url = make_mutable_copy("/ok");
|
||||
result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetOk);
|
||||
}
|
||||
|
||||
SUBCASE("Static POST routes") {
|
||||
auto match = ApiUrlParser::parse("POST", "/v1/commit");
|
||||
auto url = make_mutable_copy("/v1/commit");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("POST", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::PostCommit);
|
||||
}
|
||||
|
||||
SUBCASE("Not found") {
|
||||
auto match = ApiUrlParser::parse("GET", "/unknown/route");
|
||||
auto url = make_mutable_copy("/unknown/route");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::NotFound);
|
||||
|
||||
match = ApiUrlParser::parse("DELETE", "/v1/version");
|
||||
url = make_mutable_copy("/v1/version");
|
||||
result = ApiUrlParser::parse("DELETE", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser with query strings") {
|
||||
SUBCASE("Simple query string") {
|
||||
auto match = ApiUrlParser::parse("GET", "/v1/status?request_id=123");
|
||||
auto url = make_mutable_copy("/v1/status?request_id=123");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
REQUIRE(match.params[static_cast<size_t>(ApiParameterKey::RequestId)]
|
||||
.has_value());
|
||||
CHECK(
|
||||
match.params[static_cast<size_t>(ApiParameterKey::RequestId)].value() ==
|
||||
"123");
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"123");
|
||||
}
|
||||
|
||||
SUBCASE("Multiple query parameters") {
|
||||
auto match =
|
||||
ApiUrlParser::parse("GET", "/v1/status?request_id=abc&min_version=42");
|
||||
auto url = make_mutable_copy("/v1/status?request_id=abc&min_version=42");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
REQUIRE(match.params[static_cast<size_t>(ApiParameterKey::RequestId)]
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"abc");
|
||||
REQUIRE(match.params[static_cast<int>(ApiParameterKey::MinVersion)]
|
||||
.has_value());
|
||||
CHECK(
|
||||
match.params[static_cast<size_t>(ApiParameterKey::RequestId)].value() ==
|
||||
"abc");
|
||||
REQUIRE(match.params[static_cast<size_t>(ApiParameterKey::MinVersion)]
|
||||
.has_value());
|
||||
CHECK(match.params[static_cast<size_t>(ApiParameterKey::MinVersion)]
|
||||
.value() == "42");
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::MinVersion)].value() ==
|
||||
"42");
|
||||
}
|
||||
|
||||
SUBCASE("Unknown parameters are ignored") {
|
||||
auto match = ApiUrlParser::parse("GET", "/v1/version?foo=bar&baz=quux");
|
||||
auto url = make_mutable_copy("/v1/version?foo=bar&baz=quux");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetVersion);
|
||||
CHECK_FALSE(match.params[static_cast<size_t>(ApiParameterKey::RequestId)]
|
||||
.has_value());
|
||||
CHECK_FALSE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser with URL parameters") {
|
||||
SUBCASE("PUT retention policy") {
|
||||
auto match = ApiUrlParser::parse("PUT", "/v1/retention/my-policy");
|
||||
auto url = make_mutable_copy("/v1/retention/my-policy");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("PUT", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::PutRetention);
|
||||
REQUIRE(match.params[static_cast<size_t>(ApiParameterKey::PolicyId)]
|
||||
.has_value());
|
||||
CHECK(
|
||||
match.params[static_cast<size_t>(ApiParameterKey::PolicyId)].value() ==
|
||||
"my-policy");
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
"my-policy");
|
||||
}
|
||||
|
||||
SUBCASE("DELETE retention policy") {
|
||||
auto match = ApiUrlParser::parse("DELETE", "/v1/retention/another-policy");
|
||||
auto url = make_mutable_copy("/v1/retention/another-policy");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("DELETE", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::DeleteRetention);
|
||||
REQUIRE(match.params[static_cast<size_t>(ApiParameterKey::PolicyId)]
|
||||
.has_value());
|
||||
CHECK(
|
||||
match.params[static_cast<size_t>(ApiParameterKey::PolicyId)].value() ==
|
||||
"another-policy");
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
"another-policy");
|
||||
}
|
||||
|
||||
SUBCASE("GET retention policy") {
|
||||
auto match = ApiUrlParser::parse("GET", "/v1/retention/get-this");
|
||||
auto url = make_mutable_copy("/v1/retention/get-this");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
REQUIRE(match.params[static_cast<size_t>(ApiParameterKey::PolicyId)]
|
||||
.has_value());
|
||||
CHECK(
|
||||
match.params[static_cast<size_t>(ApiParameterKey::PolicyId)].value() ==
|
||||
"get-this");
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
"get-this");
|
||||
}
|
||||
|
||||
SUBCASE("GET all retention policies (no ID)") {
|
||||
auto match = ApiUrlParser::parse("GET", "/v1/retention");
|
||||
auto url = make_mutable_copy("/v1/retention");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
CHECK_FALSE(match.params[static_cast<size_t>(ApiParameterKey::PolicyId)]
|
||||
.has_value());
|
||||
CHECK_FALSE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser with URL and query parameters") {
|
||||
auto match = ApiUrlParser::parse("DELETE", "/v1/retention/p1?wait=true");
|
||||
auto url = make_mutable_copy("/v1/retention/p1?request_id=abc123");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("DELETE", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::DeleteRetention);
|
||||
REQUIRE(
|
||||
match.params[static_cast<size_t>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<size_t>(ApiParameterKey::PolicyId)].value() ==
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
"p1");
|
||||
REQUIRE(match.params[static_cast<size_t>(ApiParameterKey::Wait)].has_value());
|
||||
CHECK(match.params[static_cast<size_t>(ApiParameterKey::Wait)].value() ==
|
||||
"true");
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"abc123");
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser URL decoding") {
|
||||
SUBCASE("Path segment percent-decoding") {
|
||||
auto url = make_mutable_copy("/v1/retention/my%2Dpolicy");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("PUT", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::PutRetention);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
"my-policy");
|
||||
}
|
||||
|
||||
SUBCASE("Query parameter form decoding (+ to space)") {
|
||||
auto url = make_mutable_copy("/v1/status?request_id=hello+world");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"hello world");
|
||||
}
|
||||
|
||||
SUBCASE("Query parameter percent-decoding") {
|
||||
auto url = make_mutable_copy("/v1/status?request_id=hello%20world");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"hello world");
|
||||
}
|
||||
|
||||
SUBCASE("Base64-like sequences in query parameters") {
|
||||
auto url = make_mutable_copy("/v1/status?request_id=YWJj%3D");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"YWJj=");
|
||||
}
|
||||
|
||||
SUBCASE("Mixed encoding in path and query") {
|
||||
auto url = make_mutable_copy(
|
||||
"/v1/retention/my%2Dpolicy?request_id=hello+world%21");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("DELETE", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::DeleteRetention);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
"my-policy");
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"hello world!");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser malformed encoding") {
|
||||
SUBCASE("Incomplete percent sequence in path") {
|
||||
auto url = make_mutable_copy("/v1/retention/bad%2");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("PUT", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::MalformedEncoding);
|
||||
}
|
||||
|
||||
SUBCASE("Invalid hex digits in path") {
|
||||
auto url = make_mutable_copy("/v1/retention/bad%ZZ");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("PUT", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::MalformedEncoding);
|
||||
}
|
||||
|
||||
SUBCASE("Incomplete percent sequence in query") {
|
||||
auto url = make_mutable_copy("/v1/status?request_id=bad%2");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::MalformedEncoding);
|
||||
}
|
||||
|
||||
SUBCASE("Invalid hex digits in query") {
|
||||
auto url = make_mutable_copy("/v1/status?request_id=bad%GG");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::MalformedEncoding);
|
||||
}
|
||||
|
||||
SUBCASE("Percent at end of path") {
|
||||
auto url = make_mutable_copy("/v1/retention/bad%");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("PUT", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::MalformedEncoding);
|
||||
}
|
||||
|
||||
SUBCASE("Percent at end of query") {
|
||||
auto url = make_mutable_copy("/v1/status?request_id=bad%");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::MalformedEncoding);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser edge cases and bugs") {
|
||||
SUBCASE("Bug: Path boundary error - /v1/retention/ with trailing slash") {
|
||||
// BUG: Code checks length > 13 but substrings at 14, causing off-by-one
|
||||
auto url = make_mutable_copy(
|
||||
"/v1/retention/"); // length 14, exactly the boundary case
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
// This should NOT set PolicyId since it's empty, but current code might
|
||||
// have issues
|
||||
CHECK_FALSE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
}
|
||||
|
||||
SUBCASE("Bug: Empty URL handling") {
|
||||
auto url = make_mutable_copy("");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::NotFound);
|
||||
}
|
||||
|
||||
SUBCASE("Bug: Query-only URL") {
|
||||
auto url = make_mutable_copy("?request_id=123");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::NotFound);
|
||||
// Should still parse query parameters
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"123");
|
||||
}
|
||||
|
||||
SUBCASE("Bug: Consecutive delimiters in query string") {
|
||||
auto url =
|
||||
make_mutable_copy("/v1/status?&&request_id=123&&min_version=42&&");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"123");
|
||||
REQUIRE(match.params[static_cast<int>(ApiParameterKey::MinVersion)]
|
||||
.has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::MinVersion)].value() ==
|
||||
"42");
|
||||
}
|
||||
|
||||
SUBCASE("Bug: Parameter without value (should be skipped)") {
|
||||
auto url = make_mutable_copy("/v1/status?debug&request_id=123");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
// debug parameter should be ignored since it has no value
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"123");
|
||||
}
|
||||
|
||||
SUBCASE("Bug: Empty parameter value") {
|
||||
auto url = make_mutable_copy("/v1/status?request_id=");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"");
|
||||
}
|
||||
|
||||
SUBCASE("Edge: Exact length boundary for retention path") {
|
||||
// Test the exact boundary condition: "/v1/retention" is 13 chars
|
||||
auto url = make_mutable_copy("/v1/retention"); // exactly 13 characters
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
CHECK_FALSE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
}
|
||||
|
||||
SUBCASE("Edge: Minimum valid policy ID") {
|
||||
// Test one character after the boundary
|
||||
auto url =
|
||||
make_mutable_copy("/v1/retention/a"); // 15 chars total, policy_id = "a"
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
"a");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("ApiUrlParser specific bug reproduction") {
|
||||
SUBCASE("Reproduction: Path boundary math error") {
|
||||
// The bug: code checks length > 13 but substrings at 14
|
||||
// "/v1/retention" = 13 chars, "/v1/retention/" = 14 chars
|
||||
// This test should demonstrate undefined behavior or wrong results
|
||||
|
||||
auto url = make_mutable_copy("/v1/retention/");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
|
||||
// Expected behavior: should match GetRetention but NOT set PolicyId
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
|
||||
// The bug: may incorrectly extract empty string or cause buffer read error
|
||||
// Let's see what actually happens
|
||||
if (match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value()) {
|
||||
auto policy_id =
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].value();
|
||||
CHECK(policy_id.empty()); // Should be empty if set at all
|
||||
}
|
||||
}
|
||||
|
||||
SUBCASE("Reproduction: Query parsing with edge cases") {
|
||||
// Test parameter parsing with multiple edge conditions
|
||||
auto url = make_mutable_copy(
|
||||
"/v1/status?=empty_key&no_value&request_id=&min_version=42&=");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
|
||||
// Should handle empty values correctly
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"");
|
||||
|
||||
REQUIRE(match.params[static_cast<int>(ApiParameterKey::MinVersion)]
|
||||
.has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::MinVersion)].value() ==
|
||||
"42");
|
||||
}
|
||||
|
||||
SUBCASE("Reproduction: Very long input stress test") {
|
||||
// Test potential integer overflow or performance issues
|
||||
std::string long_policy_id(1000, 'x'); // 1000 character policy ID
|
||||
auto url = make_mutable_copy("/v1/retention/" + long_policy_id +
|
||||
"?request_id=" + std::string(500, 'y'));
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)].value() ==
|
||||
long_policy_id);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
std::string(500, 'y'));
|
||||
}
|
||||
|
||||
SUBCASE("Reproduction: Zero-length edge case") {
|
||||
char empty_buffer[1] = {0};
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", empty_buffer, 0, match);
|
||||
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::NotFound);
|
||||
}
|
||||
|
||||
SUBCASE("Reproduction: Null buffer edge case") {
|
||||
// This might cause undefined behavior if not handled properly
|
||||
RouteMatch match;
|
||||
char single_char = '/';
|
||||
auto result = ApiUrlParser::parse("GET", &single_char, 1, match);
|
||||
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::NotFound);
|
||||
}
|
||||
|
||||
SUBCASE("BUG: Query parser pos increment overflow") {
|
||||
// BUG: pos += pair_end + 1 can go beyond buffer bounds
|
||||
// When pair_end == query_length - pos (no & found), pos becomes
|
||||
// query_length + 1
|
||||
auto url = make_mutable_copy("/v1/status?no_ampersand_at_end");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
|
||||
// This should not crash or have undefined behavior
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetStatus);
|
||||
// Parameter should be ignored since it's not a known key
|
||||
}
|
||||
|
||||
SUBCASE("BUG: String view with potentially negative length cast") {
|
||||
// BUG: decoded_value_length is int but gets cast to size_t for string_view
|
||||
// If decode function returned negative (which it can), this could wrap
|
||||
// around
|
||||
|
||||
// We can't easily trigger the decode function to return -1 through normal
|
||||
// parsing since that's caught earlier, but this tests the edge case
|
||||
// handling
|
||||
auto url = make_mutable_copy("/v1/status?request_id=normal_value");
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
|
||||
CHECK(result == ParseResult::Success);
|
||||
REQUIRE(
|
||||
match.params[static_cast<int>(ApiParameterKey::RequestId)].has_value());
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::RequestId)].value() ==
|
||||
"normal_value");
|
||||
}
|
||||
|
||||
SUBCASE("BUG: Array bounds - PolicyId path extraction edge case") {
|
||||
// Test the boundary condition more precisely
|
||||
// "/v1/retention" = 13 chars, checking length > 13, substr(14)
|
||||
auto url = make_mutable_copy("/v1/retention/"); // exactly 14 chars
|
||||
RouteMatch match;
|
||||
auto result = ApiUrlParser::parse("GET", url.data(),
|
||||
static_cast<int>(url.size()), match);
|
||||
|
||||
CHECK(result == ParseResult::Success);
|
||||
CHECK(match.route == HttpRoute::GetRetention);
|
||||
|
||||
// path.length() = 14, so > 13 is true
|
||||
// path.substr(14) should return empty string_view
|
||||
// The bug would be if this crashes or returns invalid data
|
||||
if (match.params[static_cast<int>(ApiParameterKey::PolicyId)].has_value()) {
|
||||
CHECK(match.params[static_cast<int>(ApiParameterKey::PolicyId)]
|
||||
.value()
|
||||
.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user