chromium/net/device_bound_sessions/session_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.h"

#include "base/test/bind.h"
#include "net/cookies/cookie_constants.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/cookie_util.h"
#include "net/device_bound_sessions/proto/storage.pb.h"
#include "net/test/test_with_task_environment.h"
#include "net/url_request/url_request_context_builder.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net::device_bound_sessions {

namespace {

class SessionTest : public TestWithTaskEnvironment {
 protected:
  SessionTest() : context_(CreateTestURLRequestContextBuilder()->Build()) {}

  std::unique_ptr<URLRequestContext> context_;
};

class FakeDelegate : public URLRequest::Delegate {
  void OnReadCompleted(URLRequest* request, int bytes_read) override {}
};

constexpr net::NetworkTrafficAnnotationTag kDummyAnnotation =
    net::DefineNetworkTrafficAnnotation("dbsc_registration", "");
constexpr char kSessionId[] = "SessionId";
constexpr char kUrlString[] = "https://example.test/index.html";
const GURL kTestUrl(kUrlString);

SessionParams CreateValidParams() {
  SessionParams::Scope scope;
  std::vector<SessionParams::Credential> cookie_credentials(
      {SessionParams::Credential{"test_cookie",
                                 "Secure; Domain=example.test"}});
  SessionParams params{kSessionId, kUrlString, std::move(scope),
                       std::move(cookie_credentials)};
  return params;
}

TEST_F(SessionTest, ValidService) {
  auto session = Session::CreateIfValid(CreateValidParams(), kTestUrl);
  EXPECT_TRUE(session);
}

TEST_F(SessionTest, InvalidServiceRefreshUrl) {
  auto params = CreateValidParams();
  params.refresh_url = "";
  EXPECT_FALSE(Session::CreateIfValid(params, kTestUrl));
}

TEST_F(SessionTest, ToFromProto) {
  std::unique_ptr<Session> session =
      Session::CreateIfValid(CreateValidParams(), kTestUrl);
  ASSERT_TRUE(session);

  // Convert to proto and validate contents.
  proto::Session sproto = session->ToProto();
  EXPECT_EQ(Session::Id(sproto.id()), session->id());
  EXPECT_EQ(sproto.refresh_url(), session->refresh_url().spec());
  EXPECT_EQ(sproto.should_defer_when_expired(),
            session->should_defer_when_expired());

  // Restore session from proto and validate contents.
  std::unique_ptr<Session> restored = Session::CreateFromProto(sproto);
  ASSERT_TRUE(restored);
  EXPECT_TRUE(restored->IsEqualForTesting(*session));
}

TEST_F(SessionTest, FailCreateFromInvalidProto) {
  // Empty proto.
  {
    proto::Session sproto;
    EXPECT_FALSE(Session::CreateFromProto(sproto));
  }

  // Create a fully populated proto.
  std::unique_ptr<Session> session =
      Session::CreateIfValid(CreateValidParams(), kTestUrl);
  ASSERT_TRUE(session);
  proto::Session sproto = session->ToProto();

  // Missing fields.
  {
    proto::Session s(sproto);
    s.clear_id();
    EXPECT_FALSE(Session::CreateFromProto(s));
  }
  {
    proto::Session s(sproto);
    s.clear_refresh_url();
    EXPECT_FALSE(Session::CreateFromProto(s));
  }
  {
    proto::Session s(sproto);
    s.clear_should_defer_when_expired();
    EXPECT_FALSE(Session::CreateFromProto(s));
  }
  {
    proto::Session s(sproto);
    s.clear_expiry_time();
    EXPECT_FALSE(Session::CreateFromProto(s));
  }
  {
    proto::Session s(sproto);
    s.clear_session_inclusion_rules();
    EXPECT_FALSE(Session::CreateFromProto(s));
  }

  // Empty id.
  {
    proto::Session s(sproto);
    s.set_id("");
    EXPECT_FALSE(Session::CreateFromProto(s));
  }
  // Invalid refresh URL.
  {
    proto::Session s(sproto);
    s.set_refresh_url("blank");
    EXPECT_FALSE(Session::CreateFromProto(s));
  }
}

TEST_F(SessionTest, DeferredSession) {
  auto params = CreateValidParams();
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      kTestUrl, IDLE, new FakeDelegate(), kDummyAnnotation);
  request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));

  bool is_deferred = session->ShouldDeferRequest(request.get());
  EXPECT_TRUE(is_deferred);
}

TEST_F(SessionTest, NotDeferredAsExcluded) {
  auto params = CreateValidParams();
  SessionParams::Scope::Specification spec;
  spec.type = SessionParams::Scope::Specification::Type::kExclude;
  spec.domain = "example.test";
  spec.path = "/index.html";
  params.scope.specifications.push_back(spec);
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      kTestUrl, IDLE, new FakeDelegate(), kDummyAnnotation);
  request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));

  bool is_deferred = session->ShouldDeferRequest(request.get());
  EXPECT_FALSE(is_deferred);
}

TEST_F(SessionTest, NotDeferredSubdomain) {
  const char subdomain[] = "https://test.example.test/index.html";
  const GURL url_subdomain(subdomain);
  auto params = CreateValidParams();
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      url_subdomain, IDLE, new FakeDelegate(), kDummyAnnotation);
  request->set_site_for_cookies(SiteForCookies::FromUrl(url_subdomain));

  bool is_deferred = session->ShouldDeferRequest(request.get());
  EXPECT_FALSE(is_deferred);
}

TEST_F(SessionTest, DeferredIncludedSubdomain) {
  // Unless include site is specified, only same origin will be
  // matched even if the spec adds an include for a different
  // origin.
  const char subdomain[] = "https://test.example.test/index.html";
  const GURL url_subdomain(subdomain);
  auto params = CreateValidParams();
  SessionParams::Scope::Specification spec;
  spec.type = SessionParams::Scope::Specification::Type::kInclude;
  spec.domain = "test.example.test";
  spec.path = "/index.html";
  params.scope.specifications.push_back(spec);
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      url_subdomain, IDLE, new FakeDelegate(), kDummyAnnotation);
  request->set_site_for_cookies(SiteForCookies::FromUrl(url_subdomain));
  ASSERT_TRUE(session->ShouldDeferRequest(request.get()));
}

TEST_F(SessionTest, NotDeferredWithCookieSession) {
  auto params = CreateValidParams();
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      kTestUrl, IDLE, new FakeDelegate(), kDummyAnnotation);
  request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
  bool is_deferred = session->ShouldDeferRequest(request.get());
  EXPECT_TRUE(is_deferred);

  CookieInclusionStatus status;
  auto source = CookieSourceType::kHTTP;
  auto cookie = CanonicalCookie::Create(
      kTestUrl, "test_cookie=v;Secure; Domain=example.test", base::Time::Now(),
      std::nullopt, std::nullopt, source, &status);
  ASSERT_TRUE(cookie);
  CookieAccessResult access_result;
  request->set_maybe_sent_cookies({{*cookie.get(), access_result}});
  EXPECT_FALSE(session->ShouldDeferRequest(request.get()));
}

TEST_F(SessionTest, NotDeferredInsecure) {
  const char insecure_url[] = "http://example.test/index.html";
  const GURL test_insecure_url(insecure_url);
  auto params = CreateValidParams();
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      test_insecure_url, IDLE, new FakeDelegate(), kDummyAnnotation);
  request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));

  bool is_deferred = session->ShouldDeferRequest(request.get());
  EXPECT_FALSE(is_deferred);
}

class InsecureDelegate : public CookieAccessDelegate {
 public:
  bool ShouldTreatUrlAsTrustworthy(const GURL& url) const override {
    return true;
  }
  CookieAccessSemantics GetAccessSemantics(
      const CanonicalCookie& cookie) const override {
    return CookieAccessSemantics::UNKNOWN;
  }
  // Returns whether a cookie should be attached regardless of its SameSite
  // value vs the request context.
  bool ShouldIgnoreSameSiteRestrictions(
      const GURL& url,
      const SiteForCookies& site_for_cookies) const override {
    return true;
  }
  [[nodiscard]] std::optional<
      std::pair<FirstPartySetMetadata, FirstPartySetsCacheFilter::MatchInfo>>
  ComputeFirstPartySetMetadataMaybeAsync(
      const net::SchemefulSite& site,
      const net::SchemefulSite* top_frame_site,
      base::OnceCallback<void(FirstPartySetMetadata,
                              FirstPartySetsCacheFilter::MatchInfo)> callback)
      const override {
    return std::nullopt;
  }
  [[nodiscard]] std::optional<
      base::flat_map<net::SchemefulSite, net::FirstPartySetEntry>>
  FindFirstPartySetEntries(
      const base::flat_set<net::SchemefulSite>& sites,
      base::OnceCallback<
          void(base::flat_map<net::SchemefulSite, net::FirstPartySetEntry>)>
          callback) const override {
    return std::nullopt;
  }
};

TEST_F(SessionTest, NotDeferredNotSameSite) {
  auto params = CreateValidParams();
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      kTestUrl, IDLE, new FakeDelegate(), kDummyAnnotation);

  bool is_deferred = session->ShouldDeferRequest(request.get());
  EXPECT_FALSE(is_deferred);
}

TEST_F(SessionTest, DeferredNotSameSiteDelegate) {
  context_->cookie_store()->SetCookieAccessDelegate(
      std::make_unique<InsecureDelegate>());
  auto params = CreateValidParams();
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      kTestUrl, IDLE, new FakeDelegate(), kDummyAnnotation);

  bool is_deferred = session->ShouldDeferRequest(request.get());
  EXPECT_TRUE(is_deferred);
}

TEST_F(SessionTest, NotDeferredIncludedSubdomainHostCraving) {
  // Unless include site is specified, only same origin will be
  // matched even if the spec adds an include for a different
  // origin.
  const char subdomain[] = "https://test.example.test/index.html";
  const GURL url_subdomain(subdomain);
  auto params = CreateValidParams();
  SessionParams::Scope::Specification spec;
  spec.type = SessionParams::Scope::Specification::Type::kInclude;
  spec.domain = "test.example.test";
  spec.path = "/index.html";
  params.scope.specifications.push_back(spec);
  std::vector<SessionParams::Credential> cookie_credentials(
      {SessionParams::Credential{"test_cookie", "Secure;"}});
  params.credentials = std::move(cookie_credentials);
  auto session = Session::CreateIfValid(params, kTestUrl);
  ASSERT_TRUE(session);
  std::unique_ptr<URLRequest> request = context_->CreateRequest(
      url_subdomain, IDLE, new FakeDelegate(), kDummyAnnotation);
  request->set_site_for_cookies(SiteForCookies::FromUrl(url_subdomain));
  ASSERT_FALSE(session->ShouldDeferRequest(request.get()));
}

}  // namespace

}  // namespace net::device_bound_sessions