chromium/net/device_bound_sessions/session_inclusion_rules_unittest.cc

// 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 "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace net::device_bound_sessions {

namespace {

using Result = SessionInclusionRules::InclusionResult;

// 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.
#define ASSERT_DOMAIN_AND_REGISTRY(origin, 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.";                                 \
  }

struct EvaluateUrlTestCase {
  const char* url;
  Result expected_result;
};

void CheckEvaluateUrlTestCases(
    const SessionInclusionRules& inclusion_rules,
    std::initializer_list<EvaluateUrlTestCase> test_cases) {
  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(test_case.url);
    EXPECT_EQ(inclusion_rules.EvaluateRequestUrl(GURL(test_case.url)),
              test_case.expected_result);
  }
}

struct AddUrlRuleTestCase {
  Result rule_type;
  const char* host_pattern;
  const char* path_prefix;
  bool expected_is_added;
};

void CheckAddUrlRuleTestCases(
    SessionInclusionRules& inclusion_rules,
    std::initializer_list<AddUrlRuleTestCase> test_cases) {
  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(base::JoinString(
        {test_case.host_pattern, test_case.path_prefix}, ", "));
    bool is_added = inclusion_rules.AddUrlRuleIfValid(
        test_case.rule_type, test_case.host_pattern, test_case.path_prefix);
    EXPECT_EQ(is_added, test_case.expected_is_added);
  }
}

TEST(SessionInclusionRulesTest, DefaultConstructorMatchesNothing) {
  SessionInclusionRules inclusion_rules;
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

  EXPECT_EQ(Result::kExclude,
            inclusion_rules.EvaluateRequestUrl(GURL("https://origin.test")));
  EXPECT_EQ(Result::kExclude, inclusion_rules.EvaluateRequestUrl(GURL()));
}

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

  ASSERT_DOMAIN_AND_REGISTRY(subdomain_origin, "site.test");

  SessionInclusionRules inclusion_rules{subdomain_origin};
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

  CheckEvaluateUrlTestCases(
      inclusion_rules, {// 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(SessionInclusionRulesTest, DefaultIncludeOriginThoughMayIncludeSite) {
  url::Origin root_site_origin = url::Origin::Create(GURL("https://site.test"));

  ASSERT_DOMAIN_AND_REGISTRY(root_site_origin, "site.test");

  SessionInclusionRules inclusion_rules{root_site_origin};
  EXPECT_TRUE(inclusion_rules.may_include_site_for_testing());

  // 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(inclusion_rules,
                            {// 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(SessionInclusionRulesTest, IncludeSiteAttemptedButNotAllowed) {
  url::Origin subdomain_origin =
      url::Origin::Create(GURL("https://some.site.test"));

  ASSERT_DOMAIN_AND_REGISTRY(subdomain_origin, "site.test");

  SessionInclusionRules inclusion_rules{subdomain_origin};
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

  // Only the origin is included.
  CheckEvaluateUrlTestCases(inclusion_rules,
                            {{"https://some.site.test", Result::kInclude},
                             {"https://other.site.test", Result::kExclude}});

  // This shouldn't do anything.
  inclusion_rules.SetIncludeSite(true);
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

  // Still only the origin is included.
  CheckEvaluateUrlTestCases(inclusion_rules,
                            {{"https://some.site.test", Result::kInclude},
                             {"https://other.site.test", Result::kExclude}});
}

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

  ASSERT_DOMAIN_AND_REGISTRY(root_site_origin, "site.test");

  SessionInclusionRules inclusion_rules{root_site_origin};
  EXPECT_TRUE(inclusion_rules.may_include_site_for_testing());

  inclusion_rules.SetIncludeSite(true);

  CheckEvaluateUrlTestCases(
      inclusion_rules, {// 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(SessionInclusionRulesTest, AddUrlRuleToOriginOnly) {
  url::Origin subdomain_origin =
      url::Origin::Create(GURL("https://some.site.test"));

  ASSERT_DOMAIN_AND_REGISTRY(subdomain_origin, "site.test");

  SessionInclusionRules inclusion_rules{subdomain_origin};
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

  // Only the origin is allowed, since the setting origin is not the root
  // eTLD+1. The only acceptable rules are limited to the origin/same host.
  CheckAddUrlRuleTestCases(
      inclusion_rules,
      {// Host pattern equals origin's host. Path is valid.
       {Result::kExclude, "some.site.test", "/static", true},
       // Add an opposite rule to check later.
       {Result::kInclude, "some.site.test", "/static/included", true},
       // Path not valid.
       {Result::kExclude, "some.site.test", "NotAPath", false},
       // Other host patterns not accepted.
       {Result::kExclude, "*.site.test", "/", false},
       {Result::kExclude, "unrelated.test", "/", false},
       {Result::kExclude, "site.test", "/", false},
       {Result::kExclude, "other.site.test", "/", false},
       {Result::kExclude, "https://some.site.test", "/", false},
       {Result::kExclude, "some.site.test:443", "/", false}});

  EXPECT_EQ(inclusion_rules.num_url_rules_for_testing(), 2u);

  CheckEvaluateUrlTestCases(
      inclusion_rules,
      {// 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}});

  // 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.
  EXPECT_TRUE(inclusion_rules.AddUrlRuleIfValid(Result::kExclude,
                                                "some.site.test", "/"));
  EXPECT_EQ(Result::kExclude, inclusion_rules.EvaluateRequestUrl(GURL(
                                  "https://some.site.test/static/included")));
}

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

  ASSERT_DOMAIN_AND_REGISTRY(root_site_origin, "site.test");

  SessionInclusionRules inclusion_rules{root_site_origin};
  EXPECT_TRUE(inclusion_rules.may_include_site_for_testing());

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

  // 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(inclusion_rules,
                           {{Result::kExclude, "excluded.site.test", "/", true},
                            {Result::kInclude, "included.site.test", "/", true},
                            {Result::kExclude, "site.test", "/static", true},
                            // Rules outside of the site are not allowed.
                            {Result::kExclude, "unrelated.test", "/", false}});

  EXPECT_EQ(inclusion_rules.num_url_rules_for_testing(), 3u);

  CheckEvaluateUrlTestCases(inclusion_rules,
                            {// 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 rules applies to these URLs, so the basic
                             // rules (origin) applies.
                             {"https://other.site.test", Result::kExclude},
                             {"https://site.test/stuff", Result::kInclude}});
}

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

  ASSERT_DOMAIN_AND_REGISTRY(root_site_origin, "site.test");

  SessionInclusionRules inclusion_rules{root_site_origin};
  EXPECT_TRUE(inclusion_rules.may_include_site_for_testing());

  inclusion_rules.SetIncludeSite(true);

  // Without any rules yet, the basic rules is the site.
  CheckEvaluateUrlTestCases(inclusion_rules,
                            {{"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(inclusion_rules,
                           {{Result::kExclude, "excluded.site.test", "/", true},
                            {Result::kInclude, "included.site.test", "/", true},
                            {Result::kExclude, "site.test", "/static", true},
                            // Rules outside of the site are not allowed.
                            {Result::kExclude, "unrelated.test", "/", false}});

  EXPECT_EQ(inclusion_rules.num_url_rules_for_testing(), 3u);

  CheckEvaluateUrlTestCases(
      inclusion_rules,
      {// 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}});

  // Note that the rules are independent of "include_site", so even if that is
  // "revoked" the rules still work the same way.
  inclusion_rules.SetIncludeSite(false);
  CheckEvaluateUrlTestCases(inclusion_rules,
                            {// 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},
                             // No rules applies to these URLs, so the basic
                             // rules (which is now the origin) applies.
                             {"https://other.site.test", Result::kExclude},
                             {"https://site.test/stuff", Result::kInclude}});
}

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

  ASSERT_DOMAIN_AND_REGISTRY(root_site_origin, "site.test");

  // Use the most permissive type of inclusion_rules, to hit the interesting
  // edge cases.
  SessionInclusionRules inclusion_rules{root_site_origin};
  EXPECT_TRUE(inclusion_rules.may_include_site_for_testing());

  CheckAddUrlRuleTestCases(
      inclusion_rules,
      {// Empty host pattern not permitted.
       {Result::kExclude, "", "/", false},
       // Host pattern that is only whitespace is not permitted.
       {Result::kExclude, " ", "/", false},
       // Forbidden characters in host_pattern.
       {Result::kExclude, "https://site.test", "/", false},
       {Result::kExclude, "site.test:8888", "/", false},
       {Result::kExclude, "site.test,other.test", "/", false},
       // Non-IPv6-allowable characters within the brackets.
       {Result::kExclude, "[*.:abcd::3:4:ff]", "/", false},
       {Result::kExclude, "[1:ab+cd::3:4:ff]", "/", false},
       {Result::kExclude, "[[1:abcd::3:4:ff]]", "/", false},
       // Internal wildcard characters are forbidden in the host pattern.
       {Result::kExclude, "sub.*.site.test", "/", false},
       // Multiple wildcard characters are forbidden in the host pattern.
       {Result::kExclude, "*.sub.*.site.test", "/", false},
       // Wildcard must be followed by a dot.
       {Result::kExclude, "*site.test", "/", false},
       // Wildcard must be followed by a non-eTLD.
       {Result::kExclude, "*.com", "/", false},
       // Other sites are not allowed.
       {Result::kExclude, "unrelated.site", "/", false},
       // Other hosts with no registrable domain are not allowed.
       {Result::kExclude, "4.31.198.44", "/", false},
       {Result::kExclude, "[1:abcd::3:4:ff]", "/", false},
       {Result::kExclude, "co.uk", "/", false},
       {Result::kExclude, "com", "/", false}});
}

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

  ASSERT_DOMAIN_AND_REGISTRY(tld_origin, "");

  SessionInclusionRules inclusion_rules{tld_origin};
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

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

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

  ASSERT_DOMAIN_AND_REGISTRY(ip_origin, "");

  SessionInclusionRules inclusion_rules{ip_origin};
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

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

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

  ASSERT_DOMAIN_AND_REGISTRY(ipv6_origin, "");

  SessionInclusionRules inclusion_rules{ipv6_origin};
  EXPECT_FALSE(inclusion_rules.may_include_site_for_testing());

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

// 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, in which case:
// TODO(chlily): Support port numbers in URL rules.
TEST(SessionInclusionRulesTest, NonstandardPort) {
  url::Origin nonstandard_port_origin =
      url::Origin::Create(GURL("https://site.test:8888"));

  ASSERT_DOMAIN_AND_REGISTRY(nonstandard_port_origin, "site.test");

  SessionInclusionRules inclusion_rules{nonstandard_port_origin};
  EXPECT_TRUE(inclusion_rules.may_include_site_for_testing());

  // Without any URL rules, the default origin rule allows only the same origin.
  CheckEvaluateUrlTestCases(inclusion_rules,
                            {{"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.
  inclusion_rules.SetIncludeSite(true);
  CheckEvaluateUrlTestCases(inclusion_rules,
                            {{"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(
      inclusion_rules,
      {// The pattern is rejected due to the colon, despite being the
       // same origin.
       {Result::kExclude, "site.test:8888", "/", false},
       // A rule with the same host without port specified is accepted.
       // This rule applies to any URL with the specified host.
       {Result::kExclude, "site.test", "/", true},
       // Any explicitly specified port is rejected (due to the colon),
       // even if it's the standard one.
       {Result::kExclude, "site.test:443", "/", false}});

  EXPECT_EQ(inclusion_rules.num_url_rules_for_testing(), 1u);

  CheckEvaluateUrlTestCases(
      inclusion_rules,
      {// 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(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"));
  ASSERT_DOMAIN_AND_REGISTRY(root_site_origin, "site.test");

  SessionInclusionRules inclusion_rules{root_site_origin};
  EXPECT_TRUE(inclusion_rules.may_include_site_for_testing());
  inclusion_rules.SetIncludeSite(true);
  EXPECT_TRUE(inclusion_rules.AddUrlRuleIfValid(Result::kExclude,
                                                "excluded.site.test", "/"));
  EXPECT_TRUE(inclusion_rules.AddUrlRuleIfValid(Result::kInclude,
                                                "included.site.test", "/"));

  // Create a corresponding proto object and validate.
  proto::SessionInclusionRules proto = inclusion_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_matcher_rule(), "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_matcher_rule(), "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::unique_ptr<SessionInclusionRules> restored_inclusion_rules =
      SessionInclusionRules::CreateFromProto(proto);
  ASSERT_TRUE(restored_inclusion_rules != nullptr);
  EXPECT_EQ(*restored_inclusion_rules, inclusion_rules);
}

TEST(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"));
  SessionInclusionRules inclusion_rules{root_site_origin};
  inclusion_rules.SetIncludeSite(true);
  inclusion_rules.AddUrlRuleIfValid(Result::kExclude, "excluded.site.test",
                                    "/");
  inclusion_rules.AddUrlRuleIfValid(Result::kInclude, "included.site.test",
                                    "/");
  proto::SessionInclusionRules proto = inclusion_rules.ToProto();

  // 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_matcher_rule();
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(p));
  }
  {
    proto::SessionInclusionRules p(proto);
    p.mutable_url_rules(0)->clear_path_prefix();
    EXPECT_FALSE(SessionInclusionRules::CreateFromProto(p));
  }
}

}  // namespace

}  // namespace net::device_bound_sessions