910e62b5创建于 1月15日历史提交
// Copyright 2024 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/device_bound_sessions/session_inclusion_rules.h"

#include <initializer_list>

#include "base/strings/string_util.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/device_bound_sessions/proto/storage.pb.h"
#include "net/device_bound_sessions/session_error.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace net::device_bound_sessions {

namespace {

using Result = SessionInclusionRules::InclusionResult;
using RuleType = SessionParams::Scope::Specification::Type;

// These tests depend on the registry_controlled_domains code, so assert ahead
// of time that the eTLD+1 is what we expect, for clarity and to avoid confusing
// test failures.
void AssertDomainAndRegistry(const url::Origin& origin,
                             const std::string& expected_domain_and_registry) {
  ASSERT_EQ(
      registry_controlled_domains::GetDomainAndRegistry(
          origin, registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES),
      expected_domain_and_registry)
      << "Unexpected domain and registry.";
}

}  // namespace

class SessionInclusionRulesTest : public ::testing::Test {
 public:
  struct EvaluateUrlTestCase {
    const char* url;
    Result expected_result;
  };

  struct AddUrlRuleTestCase {
    RuleType rule_type;
    const char* host_pattern;
    const char* path_prefix;
    // This is kSuccess if there is no error expected when adding.
    SessionError::ErrorType expected_is_added_result;
  };

  SessionInclusionRulesTest() = default;

  void SetOrigin(const url::Origin& origin) { origin_ = origin; }

  void SetIncludeSite(bool include_site) {
    params_.include_site = include_site;
  }

  void CheckMayIncludeSite(bool expected_may_include_site) {
    auto inclusion_rules_or_error =
        SessionInclusionRules::Create(origin_, params_, GURL());
    ASSERT_TRUE(inclusion_rules_or_error.has_value());
    SessionInclusionRules& rules = *inclusion_rules_or_error;
    EXPECT_EQ(rules.may_include_site_for_testing(), expected_may_include_site);
  }

  void CheckEvaluateUrlTestCases(
      std::initializer_list<EvaluateUrlTestCase> test_cases) {
    auto inclusion_rules_or_error =
        SessionInclusionRules::Create(origin_, params_, GURL());
    ASSERT_TRUE(inclusion_rules_or_error.has_value());
    SessionInclusionRules& rules = *inclusion_rules_or_error;

    for (const auto& test_case : test_cases) {
      SCOPED_TRACE(test_case.url);
      EXPECT_EQ(rules.EvaluateRequestUrl(GURL(test_case.url)),
                test_case.expected_result);
    }
  }

  void CheckAddUrlRuleTestCases(
      std::initializer_list<AddUrlRuleTestCase> test_cases) {
    auto inclusion_rules_or_error =
        SessionInclusionRules::Create(origin_, params_, GURL());
    ASSERT_TRUE(inclusion_rules_or_error.has_value());

    for (const auto& test_case : test_cases) {
      SCOPED_TRACE(base::JoinString(
          {test_case.host_pattern, test_case.path_prefix}, ", "));
      params_.specifications.emplace_back(
          test_case.rule_type, test_case.host_pattern, test_case.path_prefix);
      inclusion_rules_or_error =
          SessionInclusionRules::Create(origin_, params_, GURL());
      EXPECT_EQ(inclusion_rules_or_error.has_value(),
                test_case.expected_is_added_result == SessionError::kSuccess);
      if (test_case.expected_is_added_result != SessionError::kSuccess &&
          !inclusion_rules_or_error.has_value()) {
        EXPECT_EQ(inclusion_rules_or_error.error().type,
                  test_case.expected_is_added_result);
      }
      if (!inclusion_rules_or_error.has_value()) {
        // Forget about this rule so that future rules can be evaluated.
        params_.specifications.pop_back();
      }
    }
  }

  const SessionParams::Scope& params() { return params_; }

 private:
  SessionParams::Scope params_;
  url::Origin origin_;
};

TEST_F(SessionInclusionRulesTest, DefaultIncludeOriginMayNotIncludeSite) {
  url::Origin subdomain_origin =
      url::Origin::Create(GURL("https://some.site.test"));

  AssertDomainAndRegistry(subdomain_origin, "site.test");

  SetOrigin(subdomain_origin);

  CheckMayIncludeSite(false);

  CheckEvaluateUrlTestCases(
      {// URL not valid.
       {"", Result::kExclude},
       // Origins match.
       {"https://some.site.test", Result::kInclude},
       // Path is allowed.
       {"https://some.site.test/path", Result::kInclude},
       // Not same scheme.
       {"http://some.site.test", Result::kExclude},
       // Not same host (same-site subdomain).
       {"https://some.other.site.test", Result::kExclude},
       // Not same host (superdomain).
       {"https://site.test", Result::kExclude},
       // Unrelated site.
       {"https://unrelated.test", Result::kExclude},
       // Not same port.
       {"https://some.site.test:8888", Result::kExclude}});
}

TEST_F(SessionInclusionRulesTest, DefaultIncludeOriginThoughMayIncludeSite) {
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));

  AssertDomainAndRegistry(root_site_origin, "site.test");

  SetOrigin(root_site_origin);
  CheckMayIncludeSite(true);

  // All expectations are as above. Even though including the site is allowed,
  // because the origin's host is its root eTLD+1, it is still limited to a
  // default origin inclusion_rules because it did not set include_site.
  CheckEvaluateUrlTestCases({// URL not valid.
                             {"", Result::kExclude},
                             // Origins match.
                             {"https://site.test", Result::kInclude},
                             // Path is allowed.
                             {"https://site.test/path", Result::kInclude},
                             // Not same scheme.
                             {"http://site.test", Result::kExclude},
                             // Not same host (same-site subdomain).
                             {"https://other.site.test", Result::kExclude},
                             // Not same host (superdomain).
                             {"https://test", Result::kExclude},
                             // Unrelated site.
                             {"https://unrelated.test", Result::kExclude},
                             // Not same port.
                             {"https://site.test:8888", Result::kExclude}});
}

TEST_F(SessionInclusionRulesTest, IncludeSiteAttemptedButNotAllowed) {
  url::Origin subdomain_origin =
      url::Origin::Create(GURL("https://some.site.test"));

  AssertDomainAndRegistry(subdomain_origin, "site.test");

  SessionParams::Scope params;
  params.include_site = true;
  auto rules_or_error = SessionInclusionRules::Create(
      subdomain_origin, params, GURL("https://some.site.test/refresh"));
  ASSERT_FALSE(rules_or_error.has_value());
  EXPECT_EQ(rules_or_error.error().type,
            SessionError::kInvalidScopeIncludeSite);
}

TEST_F(SessionInclusionRulesTest, IncludeSite) {
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));

  AssertDomainAndRegistry(root_site_origin, "site.test");

  SetOrigin(root_site_origin);
  CheckMayIncludeSite(true);

  SetIncludeSite(true);

  CheckEvaluateUrlTestCases(
      {// URL not valid.
       {"", Result::kExclude},
       // Origins match.
       {"https://site.test", Result::kInclude},
       // Path is allowed.
       {"https://site.test/path", Result::kInclude},
       // Not same scheme (site is schemeful).
       {"http://site.test", Result::kExclude},
       // Same-site subdomain is allowed.
       {"https://some.site.test", Result::kInclude},
       {"https://some.other.site.test", Result::kInclude},
       // Not same host (superdomain).
       {"https://test", Result::kExclude},
       // Unrelated site.
       {"https://unrelated.test", Result::kExclude},
       // Other port is allowed because whole site is included.
       {"https://site.test:8888", Result::kInclude}});
}

TEST_F(SessionInclusionRulesTest, AddUrlRuleToOriginOnly) {
  url::Origin subdomain_origin =
      url::Origin::Create(GURL("https://some.site.test"));

  AssertDomainAndRegistry(subdomain_origin, "site.test");

  SetOrigin(subdomain_origin);
  CheckMayIncludeSite(false);

  CheckAddUrlRuleTestCases(
      {// Host pattern equals origin's host. Path is valid.
       {RuleType::kExclude, "some.site.test", "/static",
        SessionError::kSuccess},
       // Add an opposite rule to check later.
       {RuleType::kInclude, "some.site.test", "/static/included",
        SessionError::kSuccess},
       // Path not valid.
       {RuleType::kExclude, "some.site.test", "NotAPath",
        SessionError::kInvalidScopeRulePath},
       // Has a valid wildcard, but the origin scoping ensures it will
       // only match some.site.test.
       {RuleType::kInclude, "*.site.test", "/static/wildcard_match/",
        SessionError::kSuccess},
       // Other host patterns are not accepted.
       {RuleType::kInclude, "unrelated.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kInclude, "site.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kInclude, "other.site.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kInclude, "https://some.site.test", "/static/https_rule/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kInclude, "some.site.test:8000", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch}});

  EXPECT_EQ(params().specifications.size(), 3u);

  CheckEvaluateUrlTestCases(
      {// Matches the rule.
       {"https://some.site.test/static", Result::kExclude},
       // A path under the rule's path prefix is subject to the rule.
       {"https://some.site.test/static/some/thing", Result::kExclude},
       // These do not match the rule, so are subject to the basic rules (the
       // origin).
       {"https://some.site.test/staticcccccccc", Result::kInclude},
       {"https://other.site.test/static", Result::kExclude},
       // The more recently added rule wins out.
       {"https://some.site.test/static/included", Result::kInclude},
       {"https://some.site.test/valid_path", Result::kInclude},
       // The wildcard matching is valid, but only matches the origin.
       {"https://some.site.test/static/wildcard_match/", Result::kInclude},
       // The origin scoping takes precedence over the rule.
       {"https://subdomain.site.test/static/wildcard_match/", Result::kExclude},
       {"https://unrelated.test/", Result::kExclude},
       {"https://site.test/", Result::kExclude},
       {"https://other.test/", Result::kExclude},
       {"https://some.site.test/static/https_rule/", Result::kExclude},
       {"https://some.site.test:8000/", Result::kExclude}});

  // Note that what matters is when the rule was added, not how specific the URL
  // path prefix is. Let's add another rule now to show that.
  CheckAddUrlRuleTestCases(
      {{RuleType::kExclude, "some.site.test", "/", SessionError::kSuccess}});
  CheckEvaluateUrlTestCases(
      {{"https://some.site.test/static/included", Result::kExclude}});
}

TEST_F(SessionInclusionRulesTest, AddUrlRuleToOriginThatMayIncludeSite) {
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));

  AssertDomainAndRegistry(root_site_origin, "site.test");

  SetOrigin(root_site_origin);
  CheckMayIncludeSite(true);

  // Without any rules yet, the basic rules is just the origin, because
  // include_site was not set.
  CheckEvaluateUrlTestCases({{"https://site.test/static", Result::kInclude},
                             {"https://other.site.test", Result::kExclude}});

  CheckAddUrlRuleTestCases(
      {{RuleType::kExclude, "excluded.site.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kInclude, "included.site.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "site.test", "/static", SessionError::kSuccess},
       {RuleType::kInclude, "unrelated.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch}});

  EXPECT_EQ(params().specifications.size(), 1u);

  CheckEvaluateUrlTestCases(
      {// Path is excluded by rule.
       {"https://site.test/static", Result::kExclude},
       // Session is origin-scoped, so this rule is ignored.
       {"https://excluded.site.test", Result::kExclude},
       // Session is origin-scoped, so this rule is ignored.
       {"https://included.site.test", Result::kExclude},
       // Rule does not apply to wrong scheme.
       {"http://included.site.test", Result::kExclude},
       // No rules applies to these URLs, so the basic
       // rules (origin) applies.
       {"https://other.site.test", Result::kExclude},
       {"https://site.test/stuff", Result::kInclude},
       // Origin scoping takes precedence over rule.
       {"https://unrelated.test/", Result::kExclude}});
}

TEST_F(SessionInclusionRulesTest, AddUrlRuleToRulesIncludingSite) {
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));

  AssertDomainAndRegistry(root_site_origin, "site.test");

  SetOrigin(root_site_origin);
  CheckMayIncludeSite(true);
  SetIncludeSite(true);

  // Without any rules yet, the basic rules is the site.
  CheckEvaluateUrlTestCases({{"https://site.test/static", Result::kInclude},
                             {"https://other.site.test", Result::kInclude}});

  // Since the origin's host is the root eTLD+1, it is allowed to set rules that
  // affect URLs other than the setting origin (but still within the site).
  CheckAddUrlRuleTestCases(
      {{RuleType::kExclude, "excluded.site.test", "/", SessionError::kSuccess},
       {RuleType::kInclude, "included.site.test", "/", SessionError::kSuccess},
       {RuleType::kExclude, "site.test", "/static", SessionError::kSuccess},
       {RuleType::kInclude, "unrelated.test", "/",
        SessionError::kScopeRuleSiteScopedHostPatternMismatch}});

  EXPECT_EQ(params().specifications.size(), 3u);

  CheckEvaluateUrlTestCases(
      {// Path is excluded by rule.
       {"https://site.test/static", Result::kExclude},
       // Rule excludes URL explicitly.
       {"https://excluded.site.test", Result::kExclude},
       // Rule includes URL explicitly.
       {"https://included.site.test", Result::kInclude},
       // Rule does not apply to wrong scheme.
       {"http://included.site.test", Result::kExclude},
       // No rule applies to these URLs, so the basic rules (site) applies.
       {"https://other.site.test", Result::kInclude},
       {"https://site.test/stuff", Result::kInclude},
       // Site scoping takes precedence over rule.
       {"https://unrelated.test/", Result::kExclude}});
}

TEST_F(SessionInclusionRulesTest, AddUrlRuleToRulesIncludingOrigin) {
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));

  AssertDomainAndRegistry(root_site_origin, "site.test");

  SetOrigin(root_site_origin);
  CheckMayIncludeSite(true);
  SetIncludeSite(false);

  CheckAddUrlRuleTestCases(
      {{RuleType::kExclude, "excluded.site.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kInclude, "included.site.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "site.test", "/static", SessionError::kSuccess},
       {RuleType::kInclude, "unrelated.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch}});

  EXPECT_EQ(params().specifications.size(), 1u);

  CheckEvaluateUrlTestCases({// Path is excluded by rule.
                             {"https://site.test/static", Result::kExclude},
                             // Rejected rules
                             {"https://excluded.site.test", Result::kExclude},
                             {"https://included.site.test", Result::kExclude},
                             // No rules applies to these URLs, so the basic
                             // rules (which is only the origin) applies.
                             {"https://other.site.test", Result::kExclude},
                             {"https://site.test/stuff", Result::kInclude},
                             // Site scoping takes precedence over rule.
                             {"https://unrelated.test/", Result::kExclude}});
}

TEST_F(SessionInclusionRulesTest, UrlRuleParsing) {
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));

  AssertDomainAndRegistry(root_site_origin, "site.test");

  // Use the most permissive type of inclusion_rules, to hit the interesting
  // edge cases.
  SetOrigin(root_site_origin);
  CheckMayIncludeSite(true);

  CheckAddUrlRuleTestCases(
      {// Empty host pattern not permitted.
       {RuleType::kExclude, "", "/",
        SessionError::kInvalidScopeRuleHostPattern},
       // Host pattern that is only whitespace is not permitted.
       {RuleType::kExclude, " ", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Forbidden characters in host_pattern.
       {RuleType::kExclude, "https://site.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "site.test:8888", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "site.test,other.test", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Non-IPv6-allowable characters within the brackets.
       {RuleType::kExclude, "[*.:abcd::3:4:ff]", "/",
        SessionError::kInvalidScopeRuleHostPattern},
       {RuleType::kExclude, "[1:ab+cd::3:4:ff]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "[[1:abcd::3:4:ff]]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Internal wildcard characters are forbidden in the host pattern.
       {RuleType::kExclude, "sub.*.site.test", "/",
        SessionError::kInvalidScopeRuleHostPattern},
       // Multiple wildcard characters are forbidden in the host pattern.
       {RuleType::kExclude, "*.sub.*.site.test", "/",
        SessionError::kInvalidScopeRuleHostPattern},
       // Wildcard must be followed by a dot.
       {RuleType::kExclude, "*site.test", "/",
        SessionError::kInvalidScopeRuleHostPattern},
       // Wildcard may be followed by an eTLD, but will only match
       // for requests matching the scope origin or requests that are
       // subdomains of the site (depending on `include_site`).
       {RuleType::kExclude, "*.test", "/", SessionError::kSuccess},
       // Other sites are not allowed.
       {RuleType::kExclude, "unrelated.site", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "4.31.198.44", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "[1:abcd::3:4:ff]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "co.uk", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "com", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch}});
}

TEST_F(SessionInclusionRulesTest, UrlRuleParsingTopLevelDomain) {
  url::Origin tld_origin = url::Origin::Create(GURL("https://com"));

  AssertDomainAndRegistry(tld_origin, "");

  SetOrigin(tld_origin);
  CheckMayIncludeSite(false);

  CheckAddUrlRuleTestCases(
      {// Exact host is allowed.
       {RuleType::kExclude, "com", "/", SessionError::kSuccess},
       // Wildcards are not permitted.
       {RuleType::kExclude, "*.com", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Other hosts with no registrable domain are not allowed.
       {RuleType::kExclude, "4.31.198.44", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "[1:abcd::3:4:ff]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "co.uk", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch}});
}

TEST_F(SessionInclusionRulesTest, UrlRuleParsingIPv4Address) {
  url::Origin ip_origin = url::Origin::Create(GURL("https://4.31.198.44"));

  AssertDomainAndRegistry(ip_origin, "");

  SetOrigin(ip_origin);
  CheckMayIncludeSite(false);

  CheckAddUrlRuleTestCases(
      {// Exact host is allowed.
       {RuleType::kExclude, "4.31.198.44", "/", SessionError::kSuccess},
       // Wildcards are not permitted for IPv4 addresses.
       {RuleType::kExclude, "*.31.198.44", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "*.4.31.198.44", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Other hosts with no registrable domain are not allowed.
       {RuleType::kExclude, "[1:abcd::3:4:ff]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "co.uk", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "com", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch}});
}

TEST_F(SessionInclusionRulesTest, UrlRuleParsingIPv6Address) {
  url::Origin ipv6_origin =
      url::Origin::Create(GURL("https://[1:abcd::3:4:ff]"));

  AssertDomainAndRegistry(ipv6_origin, "");

  SetOrigin(ipv6_origin);
  CheckMayIncludeSite(false);

  CheckAddUrlRuleTestCases(
      {// Exact host is allowed.
       {RuleType::kExclude, "[1:abcd::3:4:ff]", "/", SessionError::kSuccess},
       // Wildcards are not permitted.
       {RuleType::kExclude, "*.[1:abcd::3:4:ff]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Brackets mismatched is not allowed.
       {RuleType::kExclude, "[1:abcd::3:4:ff", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "1:abcd::3:4:ff]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Non-IPv6-allowable characters within the brackets is not allowed.
       {RuleType::kExclude, "[*.:abcd::3:4:ff]", "/",
        SessionError::kInvalidScopeRuleHostPattern},
       {RuleType::kExclude, "[1:ab+cd::3:4:ff]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "[[1:abcd::3:4:ff]]", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       // Other hosts with no registrable domain are not allowed.
       {RuleType::kExclude, "4.31.198.44", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "co.uk", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch},
       {RuleType::kExclude, "com", "/",
        SessionError::kScopeRuleOriginScopedHostPatternMismatch}});
}

// This test is more to document the current behavior than anything else. We may
// discover a need for more comprehensive support for port numbers in the
// future.
TEST_F(SessionInclusionRulesTest, NonstandardPort) {
  url::Origin nonstandard_port_origin =
      url::Origin::Create(GURL("https://site.test:8888"));

  AssertDomainAndRegistry(nonstandard_port_origin, "site.test");

  SetOrigin(nonstandard_port_origin);
  CheckMayIncludeSite(true);

  // Without any URL rules, the default origin rule allows only the same origin.
  CheckEvaluateUrlTestCases({{"https://site.test", Result::kExclude},
                             {"https://site.test:8888", Result::kInclude},
                             {"https://other.site.test", Result::kExclude}});

  // If we include_site, then same-site URLs regardless of port number are
  // included.
  SetIncludeSite(true);
  CheckEvaluateUrlTestCases({{"https://site.test", Result::kInclude},
                             {"https://site.test:8888", Result::kInclude},
                             {"https://site.test:1234", Result::kInclude},
                             {"https://other.site.test", Result::kInclude}});

  // However, adding URL rules to an inclusion_rules based on such an origin may
  // lead to unintuitive outcomes. It is not possible to specify a rule that
  // applies to the same origin as the setting origin if the setting origin has
  // a nonstandard port.
  CheckAddUrlRuleTestCases(
      {// The pattern is not accepted
       {RuleType::kInclude, "site.test:8888", "/",
        SessionError::kScopeRuleSiteScopedHostPatternMismatch},
       // A rule with the same host without port specified is accepted.
       // This rule applies to any URL with the specified host.
       {RuleType::kExclude, "site.test", "/", SessionError::kSuccess},
       // The pattern is not accepted
       {RuleType::kInclude, "site.test:443", "/",
        SessionError::kScopeRuleSiteScopedHostPatternMismatch}});

  EXPECT_EQ(params().specifications.size(), 1u);

  CheckEvaluateUrlTestCases(
      {// This is same-origin but gets caught in the "site.test" rule because
       // the rule didn't specify a port.
       {"https://site.test:8888", Result::kExclude},
       // This is same-site but gets caught in the "site.test" rule because
       // the rule didn't specify a port.
       {"https://site.test:1234", Result::kExclude},
       // Same-site is included by basic rules.
       {"https://other.site.test", Result::kInclude},
       // Also excluded explicitly by rule.
       {"https://site.test", Result::kExclude},
       {"https://site.test:443", Result::kExclude}});
}

TEST_F(SessionInclusionRulesTest, ToFromProto) {
  // Create a valid SessionInclusionRules object with default inclusion rule and
  // a couple of additional URL rules.
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));
  AssertDomainAndRegistry(root_site_origin, "site.test");

  SessionParams::Scope params;
  params.include_site = true;
  params.specifications = {{RuleType::kExclude, "excluded.site.test", "/"},
                           {RuleType::kInclude, "included.site.test", "/"}};
  auto inclusion_rules_or_error = SessionInclusionRules::Create(
      root_site_origin, std::move(params), GURL());
  ASSERT_TRUE(inclusion_rules_or_error.has_value());
  SessionInclusionRules& rules = *inclusion_rules_or_error;

  // Create a corresponding proto object and validate.
  proto::SessionInclusionRules proto = rules.ToProto();
  EXPECT_EQ(root_site_origin.Serialize(), proto.origin());
  EXPECT_TRUE(proto.do_include_site());
  ASSERT_EQ(proto.url_rules().size(), 2);
  {
    const auto& rule = proto.url_rules(0);
    EXPECT_EQ(rule.rule_type(), proto::RuleType::EXCLUDE);
    EXPECT_EQ(rule.host_pattern(), "excluded.site.test");
    EXPECT_EQ(rule.path_prefix(), "/");
  }
  {
    const auto& rule = proto.url_rules(1);
    EXPECT_EQ(rule.rule_type(), proto::RuleType::INCLUDE);
    EXPECT_EQ(rule.host_pattern(), "included.site.test");
    EXPECT_EQ(rule.path_prefix(), "/");
  }

  // Create a SessionInclusionRules object from the proto and verify
  // that it is the same as the original.
  std::optional<SessionInclusionRules> restored_inclusion_rules =
      SessionInclusionRules::CreateFromProto(proto);
  ASSERT_TRUE(restored_inclusion_rules.has_value());
  EXPECT_EQ(*restored_inclusion_rules, rules);
}

TEST_F(SessionInclusionRulesTest, FailCreateFromInvalidProto) {
  // Empty proto.
  {
    proto::SessionInclusionRules proto;
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(proto));
  }
  // Opaque origin.
  {
    proto::SessionInclusionRules proto;
    proto.set_origin("about:blank");
    proto.set_do_include_site(false);
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(proto));
  }

  // Create a fully populated proto.
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));
  SessionParams::Scope params;
  params.include_site = true;
  params.specifications = {{RuleType::kExclude, "excluded.site.test", "/"},
                           {RuleType::kInclude, "included.site.test", "/"}};
  auto inclusion_rules_or_error = SessionInclusionRules::Create(
      root_site_origin, std::move(params), GURL());
  ASSERT_TRUE(inclusion_rules_or_error.has_value());
  SessionInclusionRules& rules = *inclusion_rules_or_error;
  proto::SessionInclusionRules proto = rules.ToProto();

  // The proto must actually be valid, or none of the following tests will be
  // validating anything.
  ASSERT_TRUE(SessionInclusionRules::CreateFromProto(proto).has_value());

  // Test for missing proto fields by clearing the fields one at a time.
  {
    proto::SessionInclusionRules p(proto);
    p.clear_origin();
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(p));
  }
  {
    proto::SessionInclusionRules p(proto);
    p.clear_do_include_site();
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(p));
  }
  // URL rules with missing parameters.
  {
    proto::SessionInclusionRules p(proto);
    p.mutable_url_rules(0)->clear_rule_type();
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(p));
  }
  {
    proto::SessionInclusionRules p(proto);
    p.mutable_url_rules(0)->clear_host_pattern();
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(p));
  }
  {
    proto::SessionInclusionRules p(proto);
    p.mutable_url_rules(0)->clear_path_prefix();
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(p));
  }
}

}  // namespace net::device_bound_sessions