blob: a85bd0dd25d0216e391a479c15725fc22aa3b739 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/base/url_search_params_view.h"
#include <string>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace net {
namespace {
using ::testing::ElementsAre;
using ::testing::Pair;
TEST(UrlSearchParamsViewTest, ParseAllSearchParams) {
const GURL url("https://a.test/index.html?a=1&b=2&c=3");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("a", "1"), Pair("b", "2"), Pair("c", "3")));
}
TEST(UrlSearchParamsViewTest, ParseSearchParamUnescapeValue) {
const GURL url(R"(https://a.test/index.html?a=a%20b%20c)");
const UrlSearchParamsView search_params(url);
EXPECT_EQ(search_params.GetDecodedParamsForTesting().size(), 1u);
EXPECT_EQ(search_params.GetDecodedParamsForTesting()[0].second, "a b c");
}
TEST(UrlSearchParamsViewTest, DeleteOneSearchParams) {
const GURL url("https://a.test/index.html?a=1&b=2&c=3");
UrlSearchParamsView search_params(url);
search_params.DeleteAllWithNames({"b"});
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("a", "1"), Pair("c", "3")));
}
TEST(UrlSearchParamsViewTest, DeleteAllExceptOneSearchParams) {
const GURL url("https://a.test/index.html?a=1&b=2&c=3");
UrlSearchParamsView search_params(url);
search_params.DeleteAllExceptWithNames({"b"});
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("b", "2")));
}
TEST(UrlSearchParamsViewTest, SortSearchParams) {
const GURL url("https://a.test/index.html?c=3&b=2&a=1&c=2&a=5");
UrlSearchParamsView search_params(url);
search_params.Sort();
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("a", "1"), Pair("a", "5"), Pair("b", "2"),
Pair("c", "3"), Pair("c", "2")));
}
TEST(UrlSearchParamsViewTest, SortSearchParamsPercentEncoded) {
const GURL url("https://a.test/index.html?c=3&b=2&a=1&%63=2&a=5");
UrlSearchParamsView search_params(url);
search_params.Sort();
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("a", "1"), Pair("a", "5"), Pair("b", "2"),
Pair("c", "3"), Pair("c", "2")));
}
TEST(UrlSearchParamsViewTest, ParseSearchParamsSpacePlusAndPercentEncoded) {
const GURL url(
R"(https://a.test/index.html?c+1=3&b+%202=2&a=1&%63%201=2&a=5)");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("c 1", "3"), Pair("b 2", "2"), Pair("a", "1"),
Pair("c 1", "2"), Pair("a", "5")));
}
TEST(UrlSearchParamsViewTest, ParseSearchParamsDoubleCodePoint) {
const GURL url(R"(https://a.test/index.html?%C3%A9=foo)");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("é", "foo")));
}
TEST(UrlSearchParamsViewTest, SortSearchParamsDoubleCodePoint) {
const GURL url(R"(https://a.test/index.html?%C3%A9=f&a=2&c=4&é=b)");
UrlSearchParamsView search_params(url);
search_params.Sort();
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("a", "2"), Pair("c", "4"), Pair("é", "f"),
Pair("é", "b")));
}
TEST(UrlSearchParamsViewTest, ParseSearchParamsTripleCodePoint) {
const GURL url(R"(https://a.test/index.html?%E3%81%81=foo)");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("ぁ", "foo")));
}
TEST(UrlSearchParamsViewTest, ParseSearchParamsQuadrupleCodePoint) {
const GURL url(R"(https://a.test/index.html?%F0%90%A8%80=foo)");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("𐨀", "foo")));
}
// In case an invalid UTF-8 sequence is entered, it would be replaced with
// the U+FFFD REPLACEMENT CHARACTER: �.
TEST(UrlSearchParamsViewTest, ParseSearchParamsInvalidCodePoint) {
const GURL url(R"(https://a.test/index.html?%C3=foo)");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("�", "foo")));
}
TEST(UrlSearchParamsViewTest, ParseSearchParamsSpecialCharacters) {
// Use special characters in both `keys` and `values`.
const base::flat_map<std::string, std::string> percent_encoding = {
{"!", "%21"}, {R"(")", "%22"}, // double quote character: "
{"#", "%23"}, {"$", "%24"}, {"%", "%25"}, {"&", "%26"},
{"'", "%27"}, {"(", "%28"}, {")", "%29"}, {"*", R"(%2A)"},
{"+", R"(%2B)"}, {",", R"(%2C)"}, {"-", R"(%2D)"}, {".", R"(%2E)"},
{"/", R"(%2F)"}, {":", R"(%3A)"}, {";", "%3B"}, {"<", R"(%3C)"},
{"=", R"(%3D)"}, {">", R"(%3E)"}, {"?", R"(%3F)"}, {"@", "%40"},
{"[", "%5B"}, {R"(\)", R"(%5C)"}, {"]", R"(%5D)"}, {"^", R"(%5E)"},
{"_", R"(%5F)"}, {"`", "%60"}, {"{", "%7B"}, {"|", R"(%7C)"},
{"}", R"(%7D)"}, {"~", R"(%7E)"}, {"", ""},
};
for (const auto& [key, value] : percent_encoding) {
std::string template_url = R"(https://a.test/index.html?$key=$value)";
base::ReplaceSubstringsAfterOffset(&template_url, 0, "$key", value);
base::ReplaceSubstringsAfterOffset(&template_url, 0, "$value", value);
const GURL url(template_url);
const UrlSearchParamsView search_params = UrlSearchParamsView(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair(key, key)));
}
}
TEST(UrlSearchParamsViewTest, ParseSearchParamsEmptyKeyOrValues) {
const GURL url("https://a.test/index.html?a&b&c&d&=5&=1");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("a", ""), Pair("b", ""), Pair("c", ""),
Pair("d", ""), Pair("", "5"), Pair("", "1")));
}
TEST(UrlSearchParamsViewTest, ParseSearchParamsInvalidEscapeTest) {
const GURL url(R"(https://a.test/index.html?a=%3&%3=b)");
const UrlSearchParamsView search_params(url);
EXPECT_THAT(search_params.GetDecodedParamsForTesting(),
ElementsAre(Pair("a", "%3"), Pair("%3", "b")));
}
struct QueryPair {
std::string_view query_a;
std::string_view query_b;
};
GURL MakeUrlWithQuery(std::string_view query) {
// The trailing "#" makes GURL preserve trailing spaces. It doesn't form part
// of the query and makes no difference in other cases.
return GURL(base::StrCat({"http://a.test/?", query, "#"}));
}
TEST(UrlSearchParamsViewTest, Equals) {
static constexpr QueryPair kEqualsCases[] = {
{"a", "a="},
{"a=+", "a=%20"},
{"=&", "="},
{"%61=%62", "a=b"},
{"%c0=%c1", "%c1=%c0"},
{"%61=1&a=2", "a=1&%61=2"},
{"a=5&b=%6", "a=5&b=%6"},
};
for (const auto& [query_a, query_b] : kEqualsCases) {
const GURL url_a = MakeUrlWithQuery(query_a);
const GURL url_b = MakeUrlWithQuery(query_b);
const UrlSearchParamsView search_params_a(url_a);
const UrlSearchParamsView search_params_b(url_b);
EXPECT_EQ(search_params_a, search_params_b)
<< "a=" << url_a << ", b=" << url_b;
}
}
TEST(UrlSearchParamsViewTest, NotEquals) {
static constexpr QueryPair kNotEqualsCases[] = {
{"a=1&a=2", "a=2&a=1"},
{"a=%c2%a2", "a=%c2%a3"},
{"a", "b"},
{"%c2%a2", "%c2%a3"},
{"=&=", "="},
{"A=0", "a=0"},
{"%cO=1", "%co=1"},
{"%c0=", "=%c0"},
};
for (const auto& [query_a, query_b] : kNotEqualsCases) {
const GURL url_a = MakeUrlWithQuery(query_a);
const GURL url_b = MakeUrlWithQuery(query_b);
const UrlSearchParamsView search_params_a(url_a);
const UrlSearchParamsView search_params_b(url_b);
EXPECT_NE(search_params_a, search_params_b)
<< "a=" << url_a << ", b=" << url_b;
}
}
TEST(UrlSearchParamsViewTest, SerializeAsUtf8WithNoQuery) {
const GURL no_query("http://a.test/");
const UrlSearchParamsView params(no_query);
EXPECT_EQ(params.SerializeAsUtf8(), "");
}
TEST(UrlSearchParamsViewTest, SerializeAsUtf8Cases) {
static constexpr std::string_view kNulEqualsNul("\0=\0", 3);
static constexpr QueryPair kQueryAndExpected[] = {
{"", ""},
{"a", "a="},
{"&", "="},
{"&b", "=&b="},
{"&&", "=&="},
{"a==b", "a=%3Db"},
{"%3d=b", "%3D=b"},
{"%26&=%26", "%26=&=%26"}, // '&' is escaped in keys and values
{"+=+", " = "}, // ' ' is not escaped
{"%20=+%20+", " = "}, // %20 is ' '
{"%=%", "%25=%25"}, // Invalid escape is made valid
{"%23=%23", "%23=%23"}, // '#' is escaped
{kNulEqualsNul, "%00=%00"}, // '\0' is escaped
{"%c2%a5", "\xc2\xa5="}, // Valid UTF-8 is not escaped
{"=%c2%a5", "=\xc2\xa5"},
{"%c2=%a5", "\xef\xbf\xbd=\xef\xbf\xbd"}, // Invalid UTF-8
};
for (const auto& [query, expected] : kQueryAndExpected) {
SCOPED_TRACE(testing::Message() << "query=\"" << query << "\", expected=\""
<< expected << "\"");
const GURL url(MakeUrlWithQuery(query));
const UrlSearchParamsView params(url);
const std::string serialized = params.SerializeAsUtf8();
EXPECT_EQ(serialized, expected);
// Loop the result through GURL and UrlSearchParamsView again to verify it
// is unchanged. This property is not strictly necessary, but it is a useful
// way to guarantee that the output is SerializeAsUtf8() is an injective
// mapping w.r.t. UrlSearchParamsView objects.
const GURL url_from_serialized(MakeUrlWithQuery(serialized));
const UrlSearchParamsView recreated_params(url_from_serialized);
EXPECT_EQ(recreated_params.SerializeAsUtf8(), serialized);
}
}
} // namespace
} // namespace net