/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "mozilla/gtest/MozAssertions.h" #include "mozilla/net/NoVarySearchUtils.h" #include "nsNetUtil.h" using namespace mozilla::net; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static NoVarySearchData Parse(const char* aHeader) { return ParseNoVarySearchHeader(nsDependentCString(aHeader)); } static nsCOMPtr MakeURI(const char* aSpec) { nsCOMPtr uri; NS_NewURI(getter_AddRefs(uri), nsDependentCString(aSpec)); return uri; } static bool Equiv(const char* aURIA, const char* aURIB, const char* aHeader) { auto data = Parse(aHeader); nsCOMPtr uriA = MakeURI(aURIA); nsCOMPtr uriB = MakeURI(aURIB); return URLsAreEquivalentModuloVariationConfig(uriA, uriB, data); } // --------------------------------------------------------------------------- // ParseNoVarySearchHeader tests // --------------------------------------------------------------------------- TEST(TestNoVarySearchParse, EmptyHeader) { auto d = Parse(""); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); EXPECT_TRUE(d.varyOnKeyOrder); } TEST(TestNoVarySearchParse, InvalidSF) { auto d = Parse("!!!not valid sf!!!"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); } TEST(TestNoVarySearchParse, ParamsInvalidType) { // params with an integer value is not a valid type → ExactMatch auto d = Parse("params=42"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); } TEST(TestNoVarySearchParse, UnknownKeysOnly) { // Header with only unknown keys → ExactMatch (no params, no except) auto d = Parse("x-custom=?1, x-other=?0"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); EXPECT_TRUE(d.varyOnKeyOrder); } TEST(TestNoVarySearchParse, ParamsAndExceptTogether) { // params=(inner list) + except → invalid → ExactMatch auto d = Parse(R"(params=("a"), except=("b"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); } TEST(TestNoVarySearchParse, ExceptWithoutParams) { // except without params → invalid → ExactMatch auto d = Parse(R"(except=("a"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); } TEST(TestNoVarySearchParse, ExceptEmptyWithoutParams) { // except with empty inner list, no params → invalid → ExactMatch auto d = Parse("except=()"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); } TEST(TestNoVarySearchParse, ParamsBareToken) { // bare `params` token → IgnoreAll // https://httpwg.org/http-extensions/draft-ietf-httpbis-no-vary-search.html#section-5.1-6 auto d = Parse("params"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::IgnoreAll); EXPECT_TRUE(d.paramNames.IsEmpty()); EXPECT_TRUE(d.varyOnKeyOrder); } TEST(TestNoVarySearchParse, ParamsExplicitBooleanTrue) { // params=?1 (explicit boolean true) → IgnoreAll, same as bare params auto d = Parse("params=?1"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::IgnoreAll); EXPECT_TRUE(d.paramNames.IsEmpty()); } TEST(TestNoVarySearchParse, ParamsExplicitBooleanFalse) { // params=?0 (boolean false) → ExactMatch (not IgnoreAll) auto d = Parse("params=?0"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); } TEST(TestNoVarySearchParse, ParamsSingleItem) { auto d = Parse(R"(params=("a"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::Blocklist); ASSERT_EQ(d.paramNames.Length(), 1u); EXPECT_EQ(d.paramNames[0], "a"_ns); } TEST(TestNoVarySearchParse, ParamsMultipleItems) { auto d = Parse(R"(params=("a" "b" "c"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::Blocklist); ASSERT_EQ(d.paramNames.Length(), 3u); EXPECT_EQ(d.paramNames[0], "a"_ns); EXPECT_EQ(d.paramNames[1], "b"_ns); EXPECT_EQ(d.paramNames[2], "c"_ns); } TEST(TestNoVarySearchParse, ExceptAllowlist) { // params, except=("id") → Allowlist // https://httpwg.org/http-extensions/draft-ietf-httpbis-no-vary-search.html#section-5.1-7 auto d = Parse(R"(params, except=("id"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::Allowlist); ASSERT_EQ(d.paramNames.Length(), 1u); EXPECT_EQ(d.paramNames[0], "id"_ns); } TEST(TestNoVarySearchParse, ExceptAllowlistExplicitBoolTrue) { // params=?1 + except=(...) → Allowlist (same as bare params + except) auto d = Parse(R"(params=?1, except=("id"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::Allowlist); ASSERT_EQ(d.paramNames.Length(), 1u); EXPECT_EQ(d.paramNames[0], "id"_ns); } TEST(TestNoVarySearchParse, ExceptMultipleKeys) { auto d = Parse(R"(params, except=("id" "type"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::Allowlist); ASSERT_EQ(d.paramNames.Length(), 2u); } TEST(TestNoVarySearchParse, KeyOrderTrue) { // bare key-order → varyOnKeyOrder=false // https://httpwg.org/http-extensions/draft-ietf-httpbis-no-vary-search.html#section-5.1-4 auto d = Parse("key-order"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::ExactMatch); EXPECT_FALSE(d.varyOnKeyOrder); } TEST(TestNoVarySearchParse, KeyOrderExplicitTrue) { // key-order=?1 (explicit boolean true) → varyOnKeyOrder=false auto d = Parse("key-order=?1"); EXPECT_FALSE(d.varyOnKeyOrder); } TEST(TestNoVarySearchParse, KeyOrderExplicitFalse) { // key-order=?0 → varyOnKeyOrder=true (order matters, explicit default) auto d = Parse("key-order=?0"); EXPECT_TRUE(d.varyOnKeyOrder); } TEST(TestNoVarySearchParse, KeyOrderCombinedWithParams) { auto d = Parse(R"(params=("utm"), key-order)"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::Blocklist); ASSERT_EQ(d.paramNames.Length(), 1u); EXPECT_EQ(d.paramNames[0], "utm"_ns); EXPECT_FALSE(d.varyOnKeyOrder); } TEST(TestNoVarySearchParse, SFExtensionParamsOnItemsIgnored) { // Unknown SF parameters on items must be ignored auto d = Parse(R"(params=("a";unknown="x"))"); EXPECT_EQ(d.paramsRule, NoVarySearchData::ParamsRule::Blocklist); ASSERT_EQ(d.paramNames.Length(), 1u); EXPECT_EQ(d.paramNames[0], "a"_ns); } // --------------------------------------------------------------------------- // URLsAreEquivalentModuloVariationConfig — parametrized tests // --------------------------------------------------------------------------- struct EquivTestData { const char* description; const char* urlA; const char* urlB; const char* header; bool expected; }; static const EquivTestData kEquivTests[] = { // ExactMatch (no header) {"ExactMatch: identical URLs", "https://example.com/page?a=1&b=2", "https://example.com/page?a=1&b=2", "", true}, {"ExactMatch: different query value", "https://example.com/page?a=1&b=2", "https://example.com/page?a=1&b=3", "", false}, // Structural mismatches always return false regardless of NVS {"Different scheme", "https://example.com/page?a=1", "http://example.com/page?a=1", "params", false}, {"Different host", "https://example.com/page?a=1", "https://other.com/page?a=1", "params", false}, {"Different path", "https://example.com/page?a=1", "https://example.com/other?a=1", "params", false}, // Blocklist: params=("a") {"Blocklist: ignored param differs → HIT", "https://example.com/p?a=1&b=2", "https://example.com/p?a=99&b=2", R"(params=("a"))", true}, {"Blocklist: non-listed param differs → MISS", "https://example.com/p?a=1&b=2", "https://example.com/p?a=1&b=99", R"(params=("a"))", false}, {"Blocklist: multiple ignored params", "https://example.com/p?a=1&b=2", "https://example.com/p?a=9&b=8", R"(params=("a" "b"))", true}, // Blocklist: many params in query string, only some in NVS list {"Blocklist: many params, only listed ones ignored → HIT", "https://example.com/p?a=1&b=2&c=3&d=4&e=5", "https://example.com/p?a=9&b=8&c=3&d=4&e=5", R"(params=("a" "b"))", true}, {"Blocklist: many params, non-listed param differs → MISS", "https://example.com/p?a=1&b=2&c=3&d=4&e=5", "https://example.com/p?a=9&b=8&c=3&d=4&e=99", R"(params=("a" "b"))", false}, // Blocklist: params in query don't match any NVS listed params {"Blocklist: no query params match NVS list → treated as ExactMatch", "https://example.com/p?x=1&y=2", "https://example.com/p?x=1&y=3", R"(params=("a" "b"))", false}, {"Blocklist: no query params match NVS list, identical → HIT", "https://example.com/p?x=1&y=2", "https://example.com/p?x=1&y=2", R"(params=("a" "b"))", true}, // IgnoreAll: params {"IgnoreAll: completely different params → HIT", "https://example.com/p?a=1&b=2", "https://example.com/p?x=9&y=8", "params", true}, {"IgnoreAll: no query vs query → HIT", "https://example.com/p", "https://example.com/p?a=1&b=2", "params", true}, {"IgnoreAll: params=?1 explicit → HIT", "https://example.com/p?a=1", "https://example.com/p?b=99", "params=?1", true}, // Allowlist: params, except=("id") {"Allowlist: kept param same → HIT", "https://example.com/p?id=42&noise=abc", "https://example.com/p?id=42&noise=xyz", R"(params, except=("id"))", true}, {"Allowlist: kept param differs → MISS", "https://example.com/p?id=42", "https://example.com/p?id=99", R"(params, except=("id"))", false}, {"Allowlist: params=?1 + except, kept param same → HIT", "https://example.com/p?id=42&noise=abc", "https://example.com/p?id=42&noise=xyz", R"(params=?1, except=("id"))", true}, // key-order {"key-order: reordered params → HIT", "https://example.com/p?a=1&b=2", "https://example.com/p?b=2&a=1", "key-order", true}, {"key-order: reordered but value differs → MISS", "https://example.com/p?a=1&b=2", "https://example.com/p?b=9&a=1", "key-order", false}, {"key-order=?0: reordered → MISS", "https://example.com/p?a=1&b=2", "https://example.com/p?b=2&a=1", "key-order=?0", false}, {"key-order=?1: reordered → HIT", "https://example.com/p?a=1&b=2", "https://example.com/p?b=2&a=1", "key-order=?1", true}, {"key-order: extra param → MISS", "https://example.com/p?a=1&b=2", "https://example.com/p?a=1&b=2&c=3", "key-order", false}, // Combined {"Combined params+key-order: ignored param and order both differ → HIT", "https://example.com/p?utm=x&b=2&a=1", "https://example.com/p?a=1&b=2&utm=y", R"(params=("utm"), key-order)", true}, // Invalid SF → ExactMatch fallback {"Invalid SF (params inner list + except) → ExactMatch → MISS", "https://example.com/p?a=1", "https://example.com/p?a=2", R"(params=("a"), except=("b"))", false}, // multi-value params {"key-order: multi-value, same values reordered by key → HIT", "https://example.com/p?a=1&a=2&b=3", "https://example.com/p?b=3&a=1&a=2", "key-order", true}, {"key-order: multi-value, values for same key in different order → MISS", "https://example.com/p?a=1&a=2", "https://example.com/p?a=2&a=1", "key-order", false}, }; class TestNoVarySearchEquivalence : public testing::TestWithParam {}; TEST_P(TestNoVarySearchEquivalence, Check) { const EquivTestData& t = GetParam(); EXPECT_EQ(Equiv(t.urlA, t.urlB, t.header), t.expected) << t.description; // Equivalence must be symmetric EXPECT_EQ(Equiv(t.urlB, t.urlA, t.header), t.expected) << t.description << " (symmetric)"; } INSTANTIATE_TEST_SUITE_P(TestNoVarySearch, TestNoVarySearchEquivalence, testing::ValuesIn(kEquivTests));