blob: 02fb56a075290a4a36bdece1cddc7530b884e5c7 [file] [log] [blame]
/** @file
*
* Unit tests for XDebug plugin utility functions.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "xdebug_utils.h"
#include "xdebug_types.h"
#include "xdebug_escape.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/generators/catch_generators_range.hpp>
#include <sstream>
#include <vector>
TEST_CASE("xdebug::parse_probe_full_json_field_value basic functionality", "[xdebug][utils]")
{
xdebug::BodyEncoding_t encoding;
SECTION("Basic probe-full-json without suffix")
{
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::AUTO);
}
SECTION("Case insensitive matching")
{
REQUIRE(xdebug::parse_probe_full_json_field_value("PROBE-FULL-JSON", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::AUTO);
REQUIRE(xdebug::parse_probe_full_json_field_value("Probe-Full-Json", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::AUTO);
}
SECTION("With whitespace around value")
{
REQUIRE(xdebug::parse_probe_full_json_field_value(" probe-full-json ", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::AUTO);
REQUIRE(xdebug::parse_probe_full_json_field_value("\t\nprobe-full-json\r\n ", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::AUTO);
}
}
TEST_CASE("xdebug::parse_probe_full_json_field_value with valid suffixes", "[xdebug][utils]")
{
xdebug::BodyEncoding_t encoding;
SECTION("hex suffix")
{
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json=hex", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::HEX);
}
SECTION("escape suffix")
{
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json=escape", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::ESCAPE);
}
SECTION("nobody suffix")
{
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json=nobody", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::OMIT_BODY);
}
SECTION("Suffixes with whitespace")
{
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json = hex", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::HEX);
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json= escape ", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::ESCAPE);
REQUIRE(xdebug::parse_probe_full_json_field_value(" probe-full-json = nobody ", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::OMIT_BODY);
}
SECTION("Case insensitive suffixes")
{
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json=HEX", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::HEX);
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json=ESCAPE", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::ESCAPE);
REQUIRE(xdebug::parse_probe_full_json_field_value("probe-full-json=Nobody", encoding));
REQUIRE(encoding == xdebug::BodyEncoding_t::OMIT_BODY);
}
}
TEST_CASE("xdebug::parse_probe_full_json_field_value invalid cases", "[xdebug][utils]")
{
xdebug::BodyEncoding_t encoding;
SECTION("Not probe-full-json")
{
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("full-json", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-json", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("x-cache", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("", encoding));
}
SECTION("Invalid suffixes")
{
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json=invalid", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json=base64", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json=json", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json=none", encoding));
}
SECTION("Malformed syntax")
{
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json=", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json==hex", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json hex", encoding)); // Missing =
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-json+hex", encoding)); // Wrong separator
}
SECTION("Partial matches")
{
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("probe-full-js", encoding));
REQUIRE_FALSE(xdebug::parse_probe_full_json_field_value("robe-full-json", encoding));
}
}
TEST_CASE("xdebug::is_textual_content_type functionality", "[xdebug][utils]")
{
SECTION("Text content types")
{
REQUIRE(xdebug::is_textual_content_type("text/html"));
REQUIRE(xdebug::is_textual_content_type("text/plain"));
REQUIRE(xdebug::is_textual_content_type("text/css"));
REQUIRE(xdebug::is_textual_content_type("text/javascript"));
REQUIRE(xdebug::is_textual_content_type("text/xml"));
}
SECTION("JSON content types")
{
REQUIRE(xdebug::is_textual_content_type("application/json"));
REQUIRE(xdebug::is_textual_content_type("application/ld+json"));
REQUIRE(xdebug::is_textual_content_type("application/vnd.api+json"));
}
SECTION("XML content types")
{
REQUIRE(xdebug::is_textual_content_type("application/xml"));
REQUIRE(xdebug::is_textual_content_type("application/rss+xml"));
REQUIRE(xdebug::is_textual_content_type("application/atom+xml"));
}
SECTION("Other textual types")
{
REQUIRE(xdebug::is_textual_content_type("application/javascript"));
REQUIRE(xdebug::is_textual_content_type("text/csv"));
REQUIRE(xdebug::is_textual_content_type("text/html; charset=utf-8"));
}
SECTION("Non-textual content types")
{
REQUIRE_FALSE(xdebug::is_textual_content_type("application/octet-stream"));
REQUIRE_FALSE(xdebug::is_textual_content_type("image/jpeg"));
REQUIRE_FALSE(xdebug::is_textual_content_type("video/mp4"));
REQUIRE_FALSE(xdebug::is_textual_content_type("audio/mpeg"));
REQUIRE_FALSE(xdebug::is_textual_content_type("application/pdf"));
REQUIRE_FALSE(xdebug::is_textual_content_type("application/zip"));
}
SECTION("Case insensitive matching")
{
REQUIRE(xdebug::is_textual_content_type("TEXT/HTML"));
REQUIRE(xdebug::is_textual_content_type("Application/JSON"));
REQUIRE(xdebug::is_textual_content_type("Application/XML"));
}
SECTION("Edge cases")
{
REQUIRE_FALSE(xdebug::is_textual_content_type(""));
REQUIRE_FALSE(xdebug::is_textual_content_type("invalid"));
REQUIRE(xdebug::is_textual_content_type("contains-json-somewhere"));
REQUIRE(xdebug::is_textual_content_type("has-xml-in-name"));
}
}
// Helper function to process a string through EscapeCharForJson.
static std::string
escape_string(std::string_view input, bool full_json)
{
xdebug::EscapeCharForJson escaper(full_json);
std::stringstream ss;
for (char c : input) {
ss << escaper(c);
}
return ss.str();
}
struct EscapeTestCase {
std::string description;
bool full_json;
std::string input;
std::string expected;
};
TEST_CASE("xdebug::EscapeCharForJson escaping", "[xdebug][headers]")
{
// clang-format off
static std::vector<EscapeTestCase> const tests = {
// Single quotes are NOT escaped in either mode.
{"full JSON: single quotes are not escaped",
xdebug::FULL_JSON,
R"('self')",
R"('self')"},
{"full JSON: CSP header with multiple single-quoted directives",
xdebug::FULL_JSON,
R"(child-src blob: 'self'; connect-src 'self' 'unsafe-inline')",
R"(child-src blob: 'self'; connect-src 'self' 'unsafe-inline')"},
{"legacy: single quotes are not escaped",
!xdebug::FULL_JSON,
R"('self')",
R"('self')"},
{"legacy: CSP header with multiple single-quoted directives",
!xdebug::FULL_JSON,
R"(child-src blob: 'self'; connect-src 'self' 'unsafe-inline')",
R"(child-src blob: 'self'; connect-src 'self' 'unsafe-inline')"},
// Common escapes work the same in both modes.
{"full JSON: double quotes are escaped",
xdebug::FULL_JSON,
R"(say "hello")",
R"(say \"hello\")"},
{"legacy: double quotes are escaped",
!xdebug::FULL_JSON,
R"(say "hello")",
R"(say \"hello\")"},
{"full JSON: backslashes are escaped",
xdebug::FULL_JSON,
R"(path\to\file)",
R"(path\\to\\file)"},
{"legacy: backslashes are escaped",
!xdebug::FULL_JSON,
R"(path\to\file)",
R"(path\\to\\file)"},
{"full JSON: tab characters are escaped",
xdebug::FULL_JSON,
"line1\tline2",
R"(line1\tline2)"},
{"full JSON: backspace characters are escaped",
xdebug::FULL_JSON,
"a\bb",
R"(a\bb)"},
{"full JSON: form feed characters are escaped",
xdebug::FULL_JSON,
"a\fb",
R"(a\fb)"},
{"full JSON: plain text passes through unchanged",
xdebug::FULL_JSON,
R"(hello world)",
R"(hello world)"},
{"legacy: plain text passes through unchanged",
!xdebug::FULL_JSON,
R"(hello world)",
R"(hello world)"},
};
// clang-format on
auto const &test = GENERATE_REF(from_range(tests));
CAPTURE(test.description, test.full_json, test.input, test.expected);
std::string result = escape_string(test.input, test.full_json);
REQUIRE(result == test.expected);
}
TEST_CASE("xdebug::EscapeCharForJson backup calculation", "[xdebug][headers]")
{
struct BackupTestCase {
std::string description;
bool full_json;
std::size_t expected_backup;
};
// clang-format off
static std::vector<BackupTestCase> const tests = {
{R"(full JSON uses "," separator (backup = 2))",
xdebug::FULL_JSON,
2},
{R"(legacy uses "',\n\t'" separator (backup = 4))",
!xdebug::FULL_JSON,
4},
};
// clang-format on
auto const &test = GENERATE_REF(from_range(tests));
CAPTURE(test.description, test.full_json, test.expected_backup);
REQUIRE(xdebug::EscapeCharForJson::backup(test.full_json) == test.expected_backup);
}