// 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/registration_fetcher.h"

#include <memory>
#include <string>
#include <utility>

#include "base/base64url.h"
#include "base/check_deref.h"
#include "base/containers/map_util.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "components/unexportable_keys/background_task_origin.h"
#include "components/unexportable_keys/mock_unexportable_key_service.h"
#include "components/unexportable_keys/unexportable_key_id.h"
#include "components/unexportable_keys/unexportable_key_service.h"
#include "components/unexportable_keys/unexportable_key_service_impl.h"
#include "components/unexportable_keys/unexportable_key_task_manager.h"
#include "crypto/scoped_fake_unexportable_key_provider.h"
#include "net/base/features.h"
#include "net/base/network_anonymization_key.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_store.h"
#include "net/cookies/cookie_store_test_callbacks.h"
#include "net/cookies/parsed_cookie.h"
#include "net/device_bound_sessions/mock_session_service.h"
#include "net/device_bound_sessions/proto/storage.pb.h"
#include "net/device_bound_sessions/registration_request_param.h"
#include "net/device_bound_sessions/session_params.h"
#include "net/device_bound_sessions/session_service.h"
#include "net/device_bound_sessions/test_support.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/log/test_net_log.h"
#include "net/socket/socket_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/test_with_task_environment.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_builder.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace net::device_bound_sessions {

namespace {

using ::base::Bucket;
using ::base::BucketsAre;
using ::base::test::DictionaryHasValue;
using ::base::test::DictionaryHasValues;
using ::base::test::RunOnceCallback;
using ::base::test::ValueIs;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::DoDefault;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::Return;
using ::testing::WithArg;
using ::unexportable_keys::UnexportableSigningKeyId;

constexpr char kBasicValidJson[] =
    R"({
  "session_identifier": "session_id",
  "refresh_url": "/refresh",
  "scope": {
    "origin": "https://a.test",
    "include_site": true,
    "scope_specification" : [
      {
        "type": "include",
        "domain": "trusted.a.test",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
  }]
})";

constexpr char kSubdomainValidJsonIncludeSiteFalse[] =
    R"({
  "session_identifier": "session_id",
  "refresh_url": "/refresh",
  "scope": {
    "origin": "https://a.test",
    "include_site": false,
    "scope_specification" : [
      {
        "type": "include",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
  }]
})";

constexpr char kSessionIdentifier[] = "session_id";
constexpr char kRedirectPath[] = "/redirect";
constexpr char kChallenge[] = "test_challenge";
constexpr unexportable_keys::BackgroundTaskPriority kTaskPriority =
    unexportable_keys::BackgroundTaskPriority::kBestEffort;
constexpr char kSessionChallengeHeaderName[] = "Secure-Session-Challenge";
constexpr char kSessionResponseHeaderName[] = "Secure-Session-Response";
constexpr char kSessionIdHeaderName[] = "Sec-Secure-Session-Id";

constexpr unexportable_keys::BackgroundTaskOrigin kTaskOrigin =
    unexportable_keys::BackgroundTaskOrigin::kDeviceBoundSessionCredentials;

std::vector<crypto::SignatureVerifier::SignatureAlgorithm> CreateAlgArray() {
  return {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256,
          crypto::SignatureVerifier::SignatureAlgorithm::RSA_PKCS1_SHA256};
}

struct InvokeCallbackArgumentAction {};

class TestRegistrationCallback {
 public:
  TestRegistrationCallback() = default;

  RegistrationFetcher::RegistrationCompleteCallback callback() {
    return base::BindOnce(&TestRegistrationCallback::OnRegistrationComplete,
                          base::Unretained(this));
  }

  void WaitForCall() {
    if (outcome_.has_value()) {
      return;
    }

    base::RunLoop run_loop;

    waiting_ = true;
    closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

  const RegistrationResult& outcome() {
    EXPECT_TRUE(outcome_.has_value());
    return *outcome_;
  }

 private:
  void OnRegistrationComplete(RegistrationFetcher* fetcher,
                              RegistrationResult params) {
    EXPECT_FALSE(outcome_.has_value());

    outcome_ = std::move(params);

    if (waiting_) {
      waiting_ = false;
      std::move(closure_).Run();
    }
  }

  std::optional<RegistrationResult> outcome_ = std::nullopt;

  bool waiting_ = false;
  base::OnceClosure closure_;
};

class RegistrationTest : public TestWithTaskEnvironment {
 protected:
  RegistrationTest()
      : server_(test_server::EmbeddedTestServer::TYPE_HTTPS),
        host_resolver_(
            base::MakeRefCounted<net::RuleBasedHostResolverProc>(nullptr)) {
    host_resolver_->AddRule("*", "127.0.0.1");
    server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);

    auto context_builder = CreateTestURLRequestContextBuilder();
    auto network_delegate = std::make_unique<TestNetworkDelegate>();
    network_delegate_ = network_delegate.get();
    context_builder->set_network_delegate(std::move(network_delegate));

    auto session_service = std::make_unique<SessionServiceMock>();
    session_service_ = session_service.get();
    context_builder->set_device_bound_session_service(
        std::move(session_service));

    context_ = context_builder->Build();
  }

  unexportable_keys::UnexportableKeyService& unexportable_key_service() {
    return unexportable_key_service_;
  }

  SessionServiceMock& session_service() { return *session_service_; }

  TestNetworkDelegate* network_delegate() { return network_delegate_; }

  // In order to get HTTPS with a registered domain, use one of the sites
  // under [test_names] in net/data/ssl/scripts/ee.cnf. We arbitrarily
  // choose *.a.test.
  GURL GetBaseURL() { return server_.GetURL("a.test", "/"); }

  RegistrationRequestParam GetBasicParam(
      std::optional<GURL> url = std::nullopt) {
    if (!url) {
      url = GetBaseURL();
    }

    return RegistrationRequestParam::CreateForTesting(
        *url, /*session_identifier=*/std::nullopt, std::string(kChallenge),
        /*authorization=*/std::nullopt);
  }

  UnexportableSigningKeyId CreateSigningKey() {
    base::test::TestFuture<
        unexportable_keys::ServiceErrorOr<UnexportableSigningKeyId>>
        future;
    unexportable_key_service_.GenerateSigningKeySlowlyAsync(
        CreateAlgArray(), kTaskPriority, future.GetCallback());
    return *future.Take();
  }

  RegistrationResult FetchWithFederatedKey(RegistrationRequestParam param,
                                           UnexportableSigningKeyId key,
                                           const GURL& provider_url) {
    base::test::TestFuture<RegistrationFetcher*, RegistrationResult> future;
    std::unique_ptr<RegistrationFetcher> fetcher =
        RegistrationFetcher::CreateFetcher(
            param, session_service(), unexportable_key_service(),
            context_.get(),
            IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
            SiteForCookies(),
            /*net_log_source=*/std::nullopt,
            /*original_request_initiator=*/std::nullopt,
            unexportable_keys::BackgroundTaskPriority::kBestEffort);
    fetcher->StartFetchWithFederatedKey(param, key, provider_url,
                                        future.GetCallback());
    return std::get<1>(future.Take());
  }

  std::unique_ptr<Session> CreateTestSession(std::string session_identifier) {
    return std::move(*Session::CreateIfValid(SessionParams{
        .session_id = std::move(session_identifier),
        .fetcher_url = GetBaseURL(),
        .refresh_url = GetBaseURL().spec(),
        .scope = {.origin = url::Origin::Create(GetBaseURL()).Serialize()},
    }));
  }

  test_server::EmbeddedTestServer server_;
  std::unique_ptr<URLRequestContext> context_;
  // `session_service_` and `network_delegate_` are owned by `context_` so they
  // should be defined after it.
  raw_ptr<SessionServiceMock> session_service_;
  raw_ptr<TestNetworkDelegate> network_delegate_;

  const url::Origin kOrigin = url::Origin::Create(GURL("https://origin/"));
  unexportable_keys::UnexportableKeyTaskManager task_manager_;
  unexportable_keys::UnexportableKeyServiceImpl unexportable_key_service_{
      task_manager_, kTaskOrigin, crypto::UnexportableKeyProvider::Config()};
  scoped_refptr<net::RuleBasedHostResolverProc> host_resolver_;
};

std::unique_ptr<test_server::HttpResponse> ReturnResponse(
    HttpStatusCode code,
    std::string_view response_text,
    const test_server::HttpRequest& request) {
  auto response = std::make_unique<test_server::BasicHttpResponse>();
  response->set_code(code);
  response->set_content_type("application/json");
  response->set_content(response_text);
  return response;
}

std::unique_ptr<test_server::HttpResponse> ReturnChallengeResponse(
    HttpStatusCode code,
    std::string_view challenge) {
  auto response = std::make_unique<test_server::BasicHttpResponse>();
  response->set_code(code);
  response->AddCustomHeader(kSessionChallengeHeaderName,
                            base::StrCat({"\"", challenge, "\""}));
  return response;
}

std::unique_ptr<test_server::HttpResponse> ReturnUnauthorized(
    const test_server::HttpRequest& request) {
  return ReturnChallengeResponse(HTTP_UNAUTHORIZED, "challenge");
}

std::unique_ptr<test_server::HttpResponse> ReturnForbidden(
    const test_server::HttpRequest& request) {
  return ReturnChallengeResponse(HTTP_FORBIDDEN, "challenge");
}

std::unique_ptr<test_server::HttpResponse> ReturnTextResponse(
    const test_server::HttpRequest& request) {
  auto response = std::make_unique<test_server::BasicHttpResponse>();
  response->set_code(HTTP_OK);
  response->set_content_type("text/plain");
  response->set_content("some content");
  return response;
}

std::unique_ptr<test_server::HttpResponse> ReturnInvalidResponse(
    const test_server::HttpRequest& request) {
  return std::make_unique<test_server::RawHttpResponse>(
      "", "Not a valid HTTP response.");
}

std::unique_ptr<test_server::HttpResponse> ReturnForHostAndPath(
    std::string_view host,
    std::string_view path,
    test_server::EmbeddedTestServer::HandleRequestCallback callback,
    const test_server::HttpRequest& request) {
  // `base_url` resolved to 127.0.0.1, so get the host and port
  // from the Host header.
  auto it = request.headers.find("host");
  if (it == request.headers.end()) {
    return nullptr;
  }
  if (it->second.find(host) == std::string::npos) {
    return nullptr;
  }
  if (request.relative_url != path) {
    return nullptr;
  }

  return callback.Run(request);
}

// The .well-known will usually need to contain a port assigned
// dynamically by `EmbeddedTestServer`. We work around that by getting
// the port from `request.base_url` and replacing "$1" with the required
// port in the .well-known contents.
std::unique_ptr<test_server::HttpResponse> ReturnWellKnown(
    const std::string& contents,
    const test_server::HttpRequest& request) {
  auto response = std::make_unique<test_server::BasicHttpResponse>();
  response->set_content_type("application/json");
  response->set_code(HTTP_OK);
  response->set_content(base::ReplaceStringPlaceholders(
      contents, {request.base_url.GetPort()}, /*offsets=*/nullptr));
  return response;
}

class UnauthorizedThenSuccessResponseContainer {
 public:
  explicit UnauthorizedThenSuccessResponseContainer(
      int unauthorize_response_times)
      : error_response_times(unauthorize_response_times) {}

  std::unique_ptr<test_server::HttpResponse> Return(
      const test_server::HttpRequest& request) {
    if (run_times++ < error_response_times) {
      return ReturnUnauthorized(request);
    }
    return ReturnResponse(HTTP_OK, kBasicValidJson, request);
  }

 private:
  int run_times = 0;
  int error_response_times = 0;
};

class ForbiddenThenSuccessResponseContainer {
 public:
  explicit ForbiddenThenSuccessResponseContainer(int forbidden_response_times)
      : error_response_times(forbidden_response_times) {}

  std::unique_ptr<test_server::HttpResponse> Return(
      const test_server::HttpRequest& request) {
    if (run_times++ < error_response_times) {
      return ReturnForbidden(request);
    }
    return ReturnResponse(HTTP_OK, kBasicValidJson, request);
  }

 private:
  int run_times = 0;
  int error_response_times = 0;
};

MATCHER_P3(EqualsInclusionRule, rule_type, rule_host, rule_path, "") {
  return testing::ExplainMatchResult(
      AllOf(
          Property("rule_type", &proto::UrlRule::rule_type, Eq(rule_type)),
          Property("host_pattern", &proto::UrlRule::host_pattern,
                   Eq(rule_host)),
          Property("path_prefix", &proto::UrlRule::path_prefix, Eq(rule_path))),
      arg, result_listener);
}

MATCHER_P2(EqualsCredential, name, attributes, "") {
  ParsedCookie cookie(std::string(name) + "=value;" + attributes);
  EXPECT_TRUE(cookie.IsValid());

  proto::CookieSameSite expected_same_site;
  switch (cookie.SameSite().first) {
    case CookieSameSite::UNSPECIFIED:
      expected_same_site = proto::CookieSameSite::COOKIE_SAME_SITE_UNSPECIFIED;
      break;
    case CookieSameSite::NO_RESTRICTION:
      expected_same_site = proto::CookieSameSite::NO_RESTRICTION;
      break;
    case CookieSameSite::LAX_MODE:
      expected_same_site = proto::CookieSameSite::LAX_MODE;
      break;
    case CookieSameSite::STRICT_MODE:
      expected_same_site = proto::CookieSameSite::STRICT_MODE;
      break;
  }

  return testing::ExplainMatchResult(
      AllOf(Property("name", &proto::CookieCraving::name, Eq(name)),
            Property("domain", &proto::CookieCraving::domain,
                     Eq(cookie.Domain())),
            Property("path", &proto::CookieCraving::path, Eq(cookie.Path())),
            Property("secure", &proto::CookieCraving::secure,
                     Eq(cookie.IsSecure())),
            Property("httponly", &proto::CookieCraving::httponly,
                     Eq(cookie.IsHttpOnly())),
            Property("same_site", &proto::CookieCraving::same_site,
                     Eq(expected_same_site))),
      arg, result_listener);
}

MATCHER(IsErrorRegistrationResult, "") {
  return arg.Visit(absl::Overload{
      [result_listener](const SessionError& error) {
        *result_listener << "is a SessionError of type "
                         << static_cast<int>(error.type);
        return true;
      },
      [result_listener](const std::unique_ptr<Session>& session) {
        *result_listener << "is a valid Session"
                         << (session ? " with ID " + session->id().value()
                                     : "");
        return false;
      },
      [result_listener](RegistrationResult::NoSessionConfigChange) {
        *result_listener << "is NoSessionConfigChange";
        return false;
      }});
}

std::optional<std::string> GetRequestChallenge(
    const test_server::HttpRequest& request) {
  auto resp_iter = request.headers.find(kSessionResponseHeaderName);
  if (resp_iter == request.headers.end()) {
    return std::nullopt;
  }
  const std::string& jwt = resp_iter->second;
  std::vector<std::string> jwt_sections =
      base::SplitString(jwt, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  if (jwt_sections.size() != 3) {
    return std::nullopt;
  }
  std::string payload;
  if (!base::Base64UrlDecode(jwt_sections[1],
                             base::Base64UrlDecodePolicy::DISALLOW_PADDING,
                             &payload)) {
    return std::nullopt;
  }
  const std::optional<base::DictValue> payload_json =
      base::JSONReader::ReadDict(payload, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!payload_json.has_value()) {
    return std::nullopt;
  }
  const std::string* challenge = payload_json->FindString("jti");
  if (!challenge) {
    return std::nullopt;
  }

  return *challenge;
}

std::optional<base::DictValue> Base64UrlEncodedJsonToDict(
    std::string_view input) {
  return base::Base64UrlDecode(input,
                               base::Base64UrlDecodePolicy::DISALLOW_PADDING)
      .and_then([](base::span<const uint8_t> json) {
        return base::JSONReader::ReadDict(base::as_string_view(json),
                                          base::JSON_PARSE_CHROMIUM_EXTENSIONS);
      });
}

TEST_F(RegistrationTest, BasicSuccess) {
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating([](const test_server::HttpRequest& request) {
        auto resp_iter = request.headers.find(kSessionResponseHeaderName);
        EXPECT_TRUE(resp_iter != request.headers.end());
        if (resp_iter != request.headers.end()) {
          EXPECT_TRUE(VerifyEs256Jwt(resp_iter->second));
        }
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const Session& session = callback.outcome().SessionForTesting();
  proto::Session session_proto = session.ToProto();
  EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
  EXPECT_THAT(
      session_proto.session_inclusion_rules().url_rules(),
      ElementsAre(
          EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
                              "/only_trusted_path"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
  EXPECT_THAT(
      session_proto.cookie_cravings(),
      ElementsAre(EqualsCredential(
          "auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Registration.Network.Result", HTTP_OK, 1);
}

TEST_F(RegistrationTest, SiteForCookiesUsedForStrictCookies) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating([](const test_server::HttpRequest& request)
                              -> std::unique_ptr<test_server::HttpResponse> {
        auto response = std::make_unique<test_server::BasicHttpResponse>();
        response->set_code(HTTP_OK);
        response->AddCustomHeader(
            "Set-Cookie",
            "auth_cookie=test_value; SameSite=Strict; Secure; Path=/");
        response->set_content(kBasicValidJson);
        return response;
      }));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;
  auto param = GetBasicParam();

  // Create an IsolationInfo with empty site_for_cookies, but provide an
  // explicit valid site_for_cookies.
  url::Origin origin = url::Origin::Create(GetBaseURL());
  IsolationInfo isolation_info = IsolationInfo::Create(
      IsolationInfo::RequestType::kOther, origin, origin, SiteForCookies());
  SiteForCookies site_for_cookies = SiteForCookies::FromOrigin(origin);

  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          isolation_info, site_for_cookies,
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const Session& session = callback.outcome().SessionForTesting();
  EXPECT_EQ(session.id().value(), "session_id");

  base::test::TestFuture<const CookieList&> cookie_future;
  context_->cookie_store()->GetAllCookiesAsync(cookie_future.GetCallback());
  const CookieList& cookies = cookie_future.Get();
  ASSERT_EQ(cookies.size(), 1u);
  EXPECT_EQ(cookies[0].Name(), "auth_cookie");
}

TEST_F(RegistrationTest, VerifyTaskPriority) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  unexportable_keys::MockUnexportableKeyService mock_service;
  mock_service.DelegateToService(unexportable_key_service());

  EXPECT_CALL(
      mock_service,
      SignSlowlyAsync(
          _, _, unexportable_keys::BackgroundTaskPriority::kUserVisible, _));

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GURL("https://a.test"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);

  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), mock_service, context_.get(),
          isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kUserVisible);

  TestRegistrationCallback callback;
  fetcher->StartFetchWithExistingKey(request_param, CreateSigningKey(),
                                     callback.callback());
  callback.WaitForCall();
}

TEST_F(RegistrationTest, SigningKeyGenerationFailure) {
  unexportable_keys::MockUnexportableKeyService mock_service;

  EXPECT_CALL(mock_service, GenerateSigningKeySlowlyAsync)
      .WillOnce(RunOnceCallback<2>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GURL("https://a.test"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);

  auto fetcher = RegistrationFetcher::CreateFetcher(
      request_param, session_service(), mock_service, context_.get(),
      isolation_info, isolation_info.site_for_cookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);

  TestRegistrationCallback callback;
  fetcher->StartCreateTokenAndFetch(request_param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kSigningKeyGenerationError);
}

TEST_F(RegistrationTest, AttestationKeyGenerationFailure) {
  unexportable_keys::MockUnexportableKeyService mock_service;

  // Required because StartCreateTokenAndFetch concurrently calls both
  // GenerateSigningKeySlowlyAsync and GenerateAttestationKeySlowlyAsync.
  // Expecting it without invoking its callback is enough because the
  // attestation failure immediately aborts the fetch flow, and omitting
  // real service delegation prevents dispatching background tasks to the
  // thread pool that could outlive the test.
  EXPECT_CALL(mock_service, GenerateSigningKeySlowlyAsync);

  // Mock attestation key generation to fail
  EXPECT_CALL(mock_service, GenerateAttestationKeySlowlyAsync)
      .WillOnce(RunOnceCallback<2>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GURL("https://a.test"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt, AttestationMode::kRequired);

  auto fetcher = RegistrationFetcher::CreateFetcher(
      request_param, session_service(), mock_service, context_.get(),
      isolation_info, isolation_info.site_for_cookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);

  TestRegistrationCallback callback;
  fetcher->StartCreateTokenAndFetch(request_param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kAttestationKeyGenerationError);
}

TEST_F(RegistrationTest, AttestationSigningKeyGenerationFailure) {
  unexportable_keys::MockUnexportableKeyService mock_service;

  // Mock signing key generation to fail
  EXPECT_CALL(mock_service, GenerateSigningKeySlowlyAsync)
      .WillOnce(RunOnceCallback<2>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GURL("https://a.test"), /*session_identifier=*/std::nullopt, kChallenge,
      /*authorization=*/std::nullopt, AttestationMode::kRequired);

  auto fetcher = RegistrationFetcher::CreateFetcher(
      request_param, session_service(), mock_service, context_.get(),
      isolation_info, isolation_info.site_for_cookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);

  TestRegistrationCallback callback;
  fetcher->StartCreateTokenAndFetch(request_param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kSigningKeyGenerationError);
}

TEST_F(RegistrationTest, AttestationKeyGenerationSuccess) {
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating([](const test_server::HttpRequest& request) {
        EXPECT_TRUE(VerifyEs256Jwt(CHECK_DEREF(
            base::FindOrNull(request.headers, kSessionResponseHeaderName))));
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), /*session_identifier=*/std::nullopt,
      std::string(kChallenge),
      /*authorization=*/std::nullopt, AttestationMode::kRequired);
  auto fetcher = RegistrationFetcher::CreateFetcher(
      param, session_service(), unexportable_key_service(), context_.get(),
      IsolationInfo::CreateTransient(/*nonce=*/std::nullopt), SiteForCookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const Session& session = callback.outcome().SessionForTesting();
  EXPECT_THAT(session.maybe_unexportable_attestation_key_id(),
              ValueIs(Optional(_)));

  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Registration.Network.Result", HTTP_OK, 1);
}

TEST_F(RegistrationTest, AttestationSuccessWithChallenge) {
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  std::string outer_jwt;
  server_.RegisterRequestHandler(
      base::BindLambdaForTesting([&](const test_server::HttpRequest& request) {
        outer_jwt = CHECK_DEREF(
            base::FindOrNull(request.headers, kSessionResponseHeaderName));
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;
  auto param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), /*session_identifier=*/std::nullopt,
      std::string(kChallenge),
      /*authorization=*/std::nullopt, AttestationMode::kRequired);
  auto fetcher = RegistrationFetcher::CreateFetcher(
      param, session_service(), unexportable_key_service(), context_.get(),
      IsolationInfo::CreateTransient(/*nonce=*/std::nullopt), SiteForCookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  const Session& session = callback.outcome().SessionForTesting();
  EXPECT_THAT(session.maybe_unexportable_attestation_key_id(),
              ValueIs(Optional(_)));
  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Registration.Network.Result", HTTP_OK, 1);

  EXPECT_TRUE(VerifyEs256Jwt(outer_jwt));

  std::vector<std::string> outer_sections = base::SplitString(
      outer_jwt, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  ASSERT_EQ(outer_sections.size(), 3u);

  ASSERT_OK_AND_ASSIGN(base::DictValue outer_header,
                       Base64UrlEncodedJsonToDict(outer_sections[0]));
  EXPECT_THAT(outer_header, DictionaryHasValues(base::DictValue()
                                                    .Set("alg", "ES256")
                                                    .Set("typ", "dbsc+aik")
                                                    .Set("cty", "jwt")));
  EXPECT_TRUE(outer_header.FindDict("jwk"));

  ASSERT_OK_AND_ASSIGN(base::DictValue outer_payload,
                       Base64UrlEncodedJsonToDict(outer_sections[1]));
  EXPECT_THAT(outer_payload,
              DictionaryHasValue("aud", base::Value(GetBaseURL().spec())));

  const base::DictValue& att = CHECK_DEREF(outer_payload.FindDict("att"));
  EXPECT_THAT(att, DictionaryHasValue("fmt", base::Value("TPM")));
  EXPECT_THAT(att.FindString("stmt"), Pointee(Not(IsEmpty())));
  EXPECT_THAT(att.FindString("sig"), Pointee(Not(IsEmpty())));

  const std::string& inner_jwt = CHECK_DEREF(outer_payload.FindString("jti"));
  EXPECT_TRUE(VerifyEs256Jwt(inner_jwt));

  std::vector<std::string> inner_sections = base::SplitString(
      inner_jwt, ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  ASSERT_EQ(inner_sections.size(), 3u);

  ASSERT_OK_AND_ASSIGN(base::DictValue inner_header,
                       Base64UrlEncodedJsonToDict(inner_sections[0]));
  EXPECT_THAT(
      inner_header,
      DictionaryHasValues(
          base::DictValue().Set("alg", "ES256").Set("typ", "dbsc+jwt")));
  EXPECT_TRUE(inner_header.FindDict("jwk"));

  ASSERT_OK_AND_ASSIGN(base::DictValue inner_payload,
                       Base64UrlEncodedJsonToDict(inner_sections[1]));
  EXPECT_THAT(inner_payload,
              DictionaryHasValue("jti", base::Value(kChallenge)));
}

TEST_F(RegistrationTest, AttestationCertificationFailure) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  unexportable_keys::MockUnexportableKeyService mock_service;
  mock_service.DelegateToService(unexportable_key_service());

  EXPECT_CALL(mock_service, CertifySlowlyAsync)
      .WillOnce(RunOnceCallback<4>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GURL("https://a.test"), /*session_identifier=*/std::nullopt,
      std::string(kChallenge),
      /*authorization=*/std::nullopt, AttestationMode::kRequired);

  auto fetcher = RegistrationFetcher::CreateFetcher(
      request_param, session_service(), mock_service, context_.get(),
      isolation_info, isolation_info.site_for_cookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);

  TestRegistrationCallback callback;
  fetcher->StartCreateTokenAndFetch(request_param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kAttestationCertificationError);
}

TEST_F(RegistrationTest, AttestationInnerSigningFailure) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  unexportable_keys::MockUnexportableKeyService mock_service;
  mock_service.DelegateToService(unexportable_key_service());

  EXPECT_CALL(mock_service, SignSlowlyAsync)
      .WillOnce(RunOnceCallback<3>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GURL("https://a.test"), /*session_identifier=*/std::nullopt,
      std::string(kChallenge),
      /*authorization=*/std::nullopt, AttestationMode::kRequired);

  auto fetcher = RegistrationFetcher::CreateFetcher(
      request_param, session_service(), mock_service, context_.get(),
      isolation_info, isolation_info.site_for_cookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);

  TestRegistrationCallback callback;
  fetcher->StartCreateTokenAndFetch(request_param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kSigningError);
}

TEST_F(RegistrationTest, AttestationOuterSigningFailure) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  unexportable_keys::MockUnexportableKeyService mock_service;
  mock_service.DelegateToService(unexportable_key_service());

  EXPECT_CALL(mock_service, SignSlowlyAsync)
      .WillOnce(DoDefault())
      .WillOnce(RunOnceCallback<3>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GURL("https://a.test"), /*session_identifier=*/std::nullopt,
      std::string(kChallenge),
      /*authorization=*/std::nullopt, AttestationMode::kRequired);

  auto fetcher = RegistrationFetcher::CreateFetcher(
      request_param, session_service(), mock_service, context_.get(),
      isolation_info, isolation_info.site_for_cookies(),
      /*net_log_source=*/std::nullopt,
      /*original_request_initiator=*/std::nullopt,
      unexportable_keys::BackgroundTaskPriority::kBestEffort);

  TestRegistrationCallback callback;
  fetcher->StartCreateTokenAndFetch(request_param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kAttestationSigningError);
}

TEST_F(RegistrationTest, NoScopeJson) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }]
})";
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kMissingScope);
}

TEST_F(RegistrationTest, NoSessionIdJson) {
  constexpr char kTestingJson[] =
      R"({
  "scope": {
    "include_site": true
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }]
})";
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidSessionId);
}

TEST_F(RegistrationTest, EmptySessionIdJson) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "",
  "scope": {
    "include_site": true
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }]
})";
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidSessionId);
}

TEST_F(RegistrationTest, SpecificationNotDictJson) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true,
    "scope_specification" : [
      "type", "domain", "path"
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidScopeSpecification);
}

TEST_F(RegistrationTest, MissingPathDefaults) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "refresh_url": "/refresh",
  "scope": {
    "include_site": true,
    "scope_specification" : [
      {
        "type": "include",
        "domain": "trusted.a.test"
      },
      {
        "type": "exclude",
        "domain": "new.a.test",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "other_cookie",
    "attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const auto& session = callback.outcome().SessionForTesting();
  proto::Session session_proto = session.ToProto();
  EXPECT_THAT(
      session_proto.session_inclusion_rules().url_rules(),
      ElementsAre(
          EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test", "/"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "new.a.test",
                              "/only_trusted_path"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
}

TEST_F(RegistrationTest, MissingDomainDefaults) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "refresh_url": "/refresh",
  "scope": {
    "include_site": true,
    "scope_specification" : [
      {
        "type": "include",
        "path": "/included"
      },
      {
        "type": "exclude",
        "domain": "new.a.test",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "other_cookie",
    "attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const auto& session = callback.outcome().SessionForTesting();
  proto::Session session_proto = session.ToProto();
  EXPECT_THAT(
      session_proto.session_inclusion_rules().url_rules(),
      ElementsAre(
          EqualsInclusionRule(proto::RuleType::INCLUDE, "*", "/included"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "new.a.test",
                              "/only_trusted_path"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
}

TEST_F(RegistrationTest, MissingRefreshUrlDefault) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true,
    "scope_specification" : [
      {
        "type": "include",
        "domain": "trusted.a.test"
      },
      {
        "type": "exclude",
        "domain": "new.a.test",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "other_cookie",
    "attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const auto& session = callback.outcome().SessionForTesting();
  EXPECT_EQ(session.refresh_url(), GetBaseURL());
}

TEST_F(RegistrationTest, OneSpecTypeInvalid) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true,
    "scope_specification" : [
      {
        "type": "invalid",
        "domain": "trusted.example.com",
        "path": "/only_trusted_path"
      },
      {
        "type": "exclude",
        "domain": "new.example.com",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidScopeSpecificationType);
}

TEST_F(RegistrationTest, InvalidTypeSpecList) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "refresh_url": "/refresh",
  "scope": {
    "include_site": true,
    "scope_specification" : "missing"
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const auto& session = callback.outcome().SessionForTesting();
  proto::Session session_proto = session.ToProto();
  EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
  EXPECT_THAT(session_proto.session_inclusion_rules().url_rules(),
              ElementsAre(EqualsInclusionRule(proto::RuleType::EXCLUDE,
                                              "a.test", "/refresh")));
}

TEST_F(RegistrationTest, TypeIsNotCookie) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true
  },
  "credentials": [{
    "type": "sync auth",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidCredentialsType);
}

TEST_F(RegistrationTest, TwoTypesCookie_NotCookie) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true
  },
  "credentials": [
    {
      "type": "cookie",
      "name": "auth_cookie",
      "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
    },
    {
      "type": "sync auth",
      "name": "auth_cookie",
      "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
    }
  ]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidCredentialsType);
}

TEST_F(RegistrationTest, TwoTypesNotCookie_Cookie) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true
  },
  "credentials": [
    {
      "type": "sync auth",
      "name": "auth_cookie",
      "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
    },
    {
      "type": "cookie",
      "name": "auth_cookie",
      "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
    }
  ]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidCredentialsType);
}

TEST_F(RegistrationTest, CredEntryWithoutDict) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  },
  "test"]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidCredentialsConfig);
}

TEST_F(RegistrationTest, CredEntryWithoutAttributes) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  // Since the cookie has no attributes, it's SameSite Lax. We set
  // a same-origin initiator to avoid registration being rejected,
  auto origin = url::Origin::Create(GetBaseURL());
  auto isolation_info = net::IsolationInfo::Create(
      net::IsolationInfo::RequestType::kOther, origin, origin,
      net::SiteForCookies::FromOrigin(origin));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/origin,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, CredEntryWithEmptyName) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true
  },
  "credentials": [{
    "type": "cookie",
    "name": "",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidCredentialsEmptyName);
}

TEST_F(RegistrationTest, ReturnTextFile) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(base::BindRepeating(&ReturnTextResponse));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam params = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          params, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(params, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidConfigJson);
}

TEST_F(RegistrationTest, ReturnInvalidJson) {
  std::string invalid_json = "*{}";
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, invalid_json));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidConfigJson);
}

TEST_F(RegistrationTest, ReturnEmptyJson) {
  std::string empty_json = "{}";
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, empty_json));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidSessionId);
}

TEST_F(RegistrationTest, NetworkErrorServerShutdown) {
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  ASSERT_TRUE(server_.Start());
  GURL url = server_.GetURL("/");
  ASSERT_TRUE(server_.ShutdownAndWaitUntilComplete());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam(url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kNetError);

  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Registration.Network.Result",
      net::ERR_CONNECTION_REFUSED, 1);
}

TEST_F(RegistrationTest, NetworkErrorInvalidResponse) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(base::BindRepeating(&ReturnInvalidResponse));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kNetError);
}

TEST_F(RegistrationTest, ResponseErrorCaptured) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_NOT_FOUND, "Not Found Body"));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  SessionError::ErrorType error =
      callback.outcome().SessionErrorForTesting()->type;
  EXPECT_EQ(error, SessionError::kPersistentHttpError);
  std::optional<FailedRequest> failed_request =
      callback.outcome().SessionErrorForTesting()->failed_request;
  EXPECT_EQ(failed_request->request_url, GetBaseURL());
  EXPECT_EQ(failed_request->net_error, std::nullopt);
  EXPECT_EQ(failed_request->response_error, 404);
  EXPECT_EQ(failed_request->response_error_body, "Not Found Body");
}

TEST_F(RegistrationTest, NetErrorCaptured) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  ASSERT_TRUE(server_.Start());
  GURL url = server_.GetURL("/");
  ASSERT_TRUE(server_.ShutdownAndWaitUntilComplete());

  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam(url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  SessionError::ErrorType error =
      callback.outcome().SessionErrorForTesting()->type;
  EXPECT_EQ(error, SessionError::kNetError);
  std::optional<FailedRequest> failed_request =
      callback.outcome().SessionErrorForTesting()->failed_request;
  EXPECT_EQ(failed_request->request_url, url);
  EXPECT_EQ(failed_request->net_error, net::ERR_CONNECTION_REFUSED);
  EXPECT_EQ(failed_request->response_error, std::nullopt);
  EXPECT_EQ(failed_request->response_error_body, std::nullopt);
}

TEST_F(RegistrationTest, ServerError407) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnResponse, HTTP_PROXY_AUTHENTICATION_REQUIRED, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kNetError);
}

TEST_F(RegistrationTest, ServerError400) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kPersistentHttpError);
}

TEST_F(RegistrationTest, ServerError500) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnResponse, HTTP_INTERNAL_SERVER_ERROR, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kTransientHttpError);
}

TEST_F(RegistrationTest, ServerErrorReturnOne403ThenSuccess) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  auto* container = new ForbiddenThenSuccessResponseContainer(1);
  server_.RegisterRequestHandler(base::BindRepeating(
      &ForbiddenThenSuccessResponseContainer::Return, base::Owned(container)));
  ASSERT_TRUE(server_.Start());

  std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
  session->set_cached_challenge("challenge");
  EXPECT_CALL(
      session_service(),
      GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
      .WillRepeatedly(Return(session.get()));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, std::string(kChallenge),
      /*authorization=*/std::nullopt);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const auto& result_session = callback.outcome().SessionForTesting();
  proto::Session session_proto = result_session.ToProto();
  EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
  EXPECT_THAT(
      session_proto.session_inclusion_rules().url_rules(),
      ElementsAre(
          EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
                              "/only_trusted_path"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
  EXPECT_THAT(
      session_proto.cookie_cravings(),
      ElementsAre(EqualsCredential(
          "auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
}

std::unique_ptr<test_server::HttpResponse> ReturnRedirect(
    const std::string& location,
    const test_server::HttpRequest& request) {
  if (request.relative_url != "/") {
    return nullptr;
  }

  auto response = std::make_unique<test_server::BasicHttpResponse>();
  response->set_code(HTTP_FOUND);
  response->AddCustomHeader("Location", location);
  response->set_content("Redirected");
  response->set_content_type("text/plain");
  return std::move(response);
}

std::unique_ptr<test_server::HttpResponse> CheckRedirect(
    bool* redirect_followed_out,
    const test_server::HttpRequest& request) {
  if (request.relative_url != kRedirectPath) {
    return nullptr;
  }

  *redirect_followed_out = true;
  return ReturnResponse(HTTP_OK, kBasicValidJson, request);
}

// Should be allowed: https://a.test -> https://a.test/redirect.
TEST_F(RegistrationTest, FollowHttpsToHttpsRedirect) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  bool followed = false;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnRedirect, kRedirectPath));
  server_.RegisterRequestHandler(
      base::BindRepeating(&CheckRedirect, &followed));
  // Required to add a certificate for a.test, which is used below.
  server_.SetSSLConfig(EmbeddedTestServer::CERT_TEST_NAMES);
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam(server_.GetURL("a.test", "/"));
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_TRUE(followed);
  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, Registration_RedirectToCrossOrigin_Fails) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  bool followed = false;
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        if (request.relative_url == "/") {
          auto response = std::make_unique<test_server::BasicHttpResponse>();
          response->set_code(HTTP_FOUND);
          response->AddCustomHeader(
              "Location",
              server_.GetURL("subdomain.a.test", "/redirect_dest").spec());
          response->set_content("Redirected");
          response->set_content_type("text/plain");
          return response;
        }
        if (request.relative_url == "/redirect_dest") {
          followed = true;
          return ReturnResponse(HTTP_OK, R"json(
            {
              "session_identifier": "session_id",
              "credentials": [
                {
                  "type": "cookie",
                  "name": "auth_cookie",
                  "attributes": "Domain=.a.test; Path=/; Secure; SameSite=None"
                }
              ],
              "scope": {
                "origin": "https://a.test",
                "include_site": false
              }
            }
          )json",
                                request);
        }
        return nullptr;
      }));

  server_.SetSSLConfig(EmbeddedTestServer::CERT_TEST_NAMES);
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam(server_.GetURL("a.test", "/"));
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());

  callback.WaitForCall();

  EXPECT_TRUE(followed);
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kCrossOriginRegistrationSiteNotIncluded);
}

TEST_F(RegistrationTest, FailOnSslErrorExpired) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  server_.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED);
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  RegistrationRequestParam param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());

  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kNetError);
}

std::unique_ptr<test_server::HttpResponse> ReturnResponseForRefreshRequest(
    const test_server::HttpRequest& request) {
  auto response = std::make_unique<test_server::BasicHttpResponse>();

  auto resp_iter = request.headers.find(kSessionResponseHeaderName);
  std::string session_response =
      resp_iter != request.headers.end() ? resp_iter->second : "";
  if (session_response.empty()) {
    const auto session_iter = request.headers.find(kSessionIdHeaderName);
    EXPECT_TRUE(session_iter != request.headers.end() &&
                !session_iter->second.empty());

    response->set_code(HTTP_FORBIDDEN);
    response->AddCustomHeader(kSessionChallengeHeaderName,
                              R"("test_challenge";id="session_id")");
    return response;
  }

  response->set_code(HTTP_OK);
  response->set_content_type("application/json");
  response->set_content(kBasicValidJson);
  return response;
}

TEST_F(RegistrationTest, BasicSuccessForExistingKey) {
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  const auto& session = callback.outcome().SessionForTesting();
  proto::Session session_proto = session.ToProto();
  EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
  EXPECT_THAT(
      session_proto.session_inclusion_rules().url_rules(),
      ElementsAre(
          EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
                              "/only_trusted_path"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
  EXPECT_THAT(
      session_proto.cookie_cravings(),
      ElementsAre(EqualsCredential(
          "auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));

  histogram_tester.ExpectBucketCount(
      "Net.DeviceBoundSessions.Refresh.Network.Result", HTTP_OK, 1);
}

TEST_F(RegistrationTest, FetchRegistrationWithCachedChallenge) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponseForRefreshRequest));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  const auto& session = callback.outcome().SessionForTesting();
  proto::Session session_proto = session.ToProto();
  EXPECT_TRUE(session_proto.session_inclusion_rules().do_include_site());
  EXPECT_THAT(
      session_proto.session_inclusion_rules().url_rules(),
      ElementsAre(
          EqualsInclusionRule(proto::RuleType::INCLUDE, "trusted.a.test",
                              "/only_trusted_path"),
          EqualsInclusionRule(proto::RuleType::EXCLUDE, "a.test", "/refresh")));
  EXPECT_THAT(
      session_proto.cookie_cravings(),
      ElementsAre(EqualsCredential(
          "auth_cookie", "Domain=.a.test; Path=/; Secure; SameSite=None")));
}

std::unique_ptr<test_server::HttpResponse> CaptureFetchMetadataHeaders(
    test_server::HttpRequest::HeaderMap* out_headers,
    const test_server::HttpRequest& request) {
  for (const char* name :
       {"Origin", "Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"}) {
    auto it = request.headers.find(name);
    if (it != request.headers.end()) {
      (*out_headers)[name] = it->second;
    }
  }
  return ReturnResponse(HTTP_OK, kBasicValidJson, request);
}

TEST_F(RegistrationTest, RefreshSendsOriginAndFetchMetadata) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  test_server::HttpRequest::HeaderMap received_headers;
  server_.RegisterRequestHandler(base::BindRepeating(
      &CaptureFetchMetadataHeaders, base::Unretained(&received_headers)));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(),
          std::ref(unexportable_key_service()), context_.get(),
          std::ref(isolation_info), net::SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  callback.outcome().SessionForTesting();

  EXPECT_THAT(
      received_headers,
      Contains(Pair("Origin", url::Origin::Create(GetBaseURL()).Serialize())));
  EXPECT_THAT(received_headers,
              Contains(Pair("Sec-Fetch-Site", "same-origin")));
  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
}

TEST_F(RegistrationTest, RefreshSendsSameSiteFetchMetadataForCrossOrigin) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  test_server::HttpRequest::HeaderMap received_headers;
  server_.RegisterRequestHandler(base::BindRepeating(
      &CaptureFetchMetadataHeaders, base::Unretained(&received_headers)));
  ASSERT_TRUE(server_.Start());

  // The session is scoped to a sibling origin within the same site as the
  // refresh endpoint.
  url::Origin scope_origin =
      url::Origin::Create(server_.GetURL("other.a.test", "/"));

  TestRegistrationCallback callback;
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt, AttestationMode::kNone, scope_origin);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(),
          std::ref(unexportable_key_service()), context_.get(),
          std::ref(isolation_info), net::SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  callback.outcome().SessionForTesting();

  EXPECT_THAT(received_headers,
              Contains(Pair("Origin", scope_origin.Serialize())));
  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Site", "same-site")));
  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
}

TEST_F(RegistrationTest, RegistrationSendsOriginAndFetchMetadata) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  test_server::HttpRequest::HeaderMap received_headers;
  server_.RegisterRequestHandler(base::BindRepeating(
      &CaptureFetchMetadataHeaders, base::Unretained(&received_headers)));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;
  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          net::SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  callback.outcome().SessionForTesting();

  EXPECT_THAT(
      received_headers,
      Contains(Pair("Origin", url::Origin::Create(GetBaseURL()).Serialize())));
  EXPECT_THAT(received_headers,
              Contains(Pair("Sec-Fetch-Site", "same-origin")));
  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
  EXPECT_THAT(received_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
}

TEST_F(RegistrationTest, FetchRegistrationAndChallengeRequired) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(base::BindRepeating(&ReturnForbidden));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), /*session_identifier=*/std::nullopt, kChallenge,
      /*authorization=*/std::nullopt);
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kRegistrationAttemptedChallenge);
}

TEST_F(RegistrationTest, FetchRefreshAndChallengeRequired_NoChallenge) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(base::BindRepeating(&ReturnForbidden));
  ASSERT_TRUE(server_.Start());

  std::unique_ptr<Session> session = CreateTestSession("session_identifier");
  EXPECT_CALL(
      session_service(),
      GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
      .WillRepeatedly(Return(session.get()));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), "session_identifier", kChallenge,
      /*authorization=*/std::nullopt);
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kInvalidChallenge);
}

TEST_F(RegistrationTest,
       FetchRefreshAndChallengeRequired_NoChallengeToNewChallenge) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  ASSERT_TRUE(server_.InitializeAndListen());
  std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
  EXPECT_CALL(
      session_service(),
      GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
      .WillOnce(Return(session.get()));
  server_.RegisterRequestHandler(base::BindRepeating(
      [](Session* session, const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        auto response = std::make_unique<test_server::BasicHttpResponse>();
        const std::optional<std::string> challenge =
            GetRequestChallenge(request);
        if (!challenge.has_value()) {
          response->set_code(HTTP_FORBIDDEN);
          session->set_cached_challenge("updated_challenge");
          return response;
        }

        if (*challenge == "updated_challenge") {
          response->set_code(HTTP_OK);
          response->set_content_type("application/json");
          response->set_content(kBasicValidJson);
          return response;
        }

        response->set_code(HTTP_FORBIDDEN);
        return response;
      },
      session.get()));
  server_.StartAcceptingConnections();

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto request_param = RegistrationRequestParam::CreateForRefresh(*session);
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest,
       FetchRefreshAndChallengeRequired_ExistingChallengeToNewChallenge) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  ASSERT_TRUE(server_.InitializeAndListen());
  std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
  EXPECT_CALL(
      session_service(),
      GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
      .WillOnce(Return(session.get()));
  server_.RegisterRequestHandler(base::BindRepeating(
      [](Session* session, const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        auto response = std::make_unique<test_server::BasicHttpResponse>();
        const std::optional<std::string> challenge =
            GetRequestChallenge(request);
        if (*challenge == kChallenge) {
          response->set_code(HTTP_FORBIDDEN);
          session->set_cached_challenge("updated_challenge");
          return response;
        }

        if (*challenge == "updated_challenge") {
          response->set_code(HTTP_OK);
          response->set_content_type("application/json");
          response->set_content(kBasicValidJson);
          return response;
        }

        response->set_code(HTTP_FORBIDDEN);
        return response;
      },
      session.get()));
  server_.StartAcceptingConnections();

  session->set_cached_challenge(kChallenge);

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto request_param = RegistrationRequestParam::CreateForRefresh(*session);
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, ContinueFalse) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "continue": false
})";
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kServerRequestedTermination);
}

TEST_F(RegistrationTest, TerminateSessionOnRepeatedFailure_Refresh) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  unexportable_keys::MockUnexportableKeyService mock_service;
  mock_service.DelegateToService(unexportable_key_service());
  EXPECT_CALL(mock_service, SignSlowlyAsync)
      .WillRepeatedly(base::test::RunOnceCallbackRepeatedly<3>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), mock_service, context_.get(),
          isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kSigningError);
}

TEST_F(RegistrationTest, TerminateSessionOnRepeatedFailure_Registration) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  unexportable_keys::MockUnexportableKeyService mock_service;
  mock_service.DelegateToService(unexportable_key_service());
  EXPECT_CALL(mock_service, SignSlowlyAsync)
      .WillRepeatedly(base::test::RunOnceCallbackRepeatedly<3>(
          base::unexpected(unexportable_keys::ServiceError::kCryptoApiFailed)));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), /*session_identifier=*/std::nullopt, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), mock_service, context_.get(),
          isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kSigningError);
}

TEST_F(RegistrationTest, NetLogRegistrationResultLogged) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;
  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(net_log_observer
                .GetEntriesWithType(NetLogEventType::DBSC_REGISTRATION_RESULT)
                .size(),
            1u);
}

TEST_F(RegistrationTest, NetLogRefreshResultLogged) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;
  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(
      net_log_observer.GetEntriesWithType(NetLogEventType::DBSC_REFRESH_RESULT)
          .size(),
      1u);
}

TEST_F(RegistrationTest, TerminateSessionOnRepeatedChallenge) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  auto* container = new ForbiddenThenSuccessResponseContainer(100);
  server_.RegisterRequestHandler(base::BindRepeating(
      &ForbiddenThenSuccessResponseContainer::Return, base::Owned(container)));
  ASSERT_TRUE(server_.Start());

  std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
  session->set_cached_challenge("challenge");
  EXPECT_CALL(
      session_service(),
      GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
      .WillRepeatedly(Return(session.get()));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kTooManyChallenges);
}

TEST_F(RegistrationTest, RefreshCachesSignedChallenge) {
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  // No cached challenge initially.
  EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge(_))
      .WillOnce(Return(nullptr));
  // Expect a signing occurrence and the new signed challenge to be cached.
  EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(1);
  EXPECT_CALL(session_service(), SetLatestSignedRefreshChallenge(_, _))
      .Times(1);
  // Quota check should return false to allow signing.
  EXPECT_CALL(session_service(), SigningQuotaExceeded(_))
      .WillOnce(Return(false));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, CreateSigningKey(),
                                     callback.callback());
  callback.WaitForCall();

  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, RefreshCachedSignedChallengeUsed) {
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  // No calls to actual signing.
  unexportable_keys::MockUnexportableKeyService mock_key_service;
  EXPECT_CALL(mock_key_service, GetAlgorithm).Times(0);
  EXPECT_CALL(mock_key_service, GetSubjectPublicKeyInfo).Times(0);
  EXPECT_CALL(mock_key_service, SignSlowlyAsync).Times(0);

  // Create a matching cached challenge.
  SessionService::SignedRefreshChallenge cached_challenge;
  cached_challenge.challenge = kChallenge;
  cached_challenge.signed_challenge = "mock_signed_challenge";

  EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge(_))
      .WillOnce(Return(&cached_challenge));
  // There should be no signing or quota checking since that's skipped.
  EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(0);
  EXPECT_CALL(session_service(), SigningQuotaExceeded(_)).Times(0);

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), mock_key_service, context_.get(),
          isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, cached_challenge.key_id,
                                     callback.callback());
  callback.WaitForCall();

  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, RefreshCachedSignedChallengeDoesNotMatch) {
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  // Add cached signed challenge that doesn't match (the challenge used is
  // different).
  SessionService::SignedRefreshChallenge cached_challenge;
  cached_challenge.challenge = "different_challenge";
  cached_challenge.signed_challenge = "mock_signed_challenge";
  EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge(_))
      .WillOnce(Return(&cached_challenge));
  EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(1);
  EXPECT_CALL(session_service(), SetLatestSignedRefreshChallenge(_, _))
      .Times(1);
  EXPECT_CALL(session_service(), SigningQuotaExceeded(_))
      .WillOnce(Return(false));

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, CreateSigningKey(),
                                     callback.callback());
  callback.WaitForCall();

  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, RegistrationTriggersSigningOccurrence) {
  // Expect the signing checks are done only when the feature is enabled.
  EXPECT_CALL(session_service(), AddSigningOccurrence(_)).Times(1);
  EXPECT_CALL(session_service(), SigningQuotaExceeded(_))
      .WillOnce(Return(false));

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating([](const test_server::HttpRequest& request) {
        auto resp_iter = request.headers.find(kSessionResponseHeaderName);
        EXPECT_TRUE(resp_iter != request.headers.end());
        if (resp_iter != request.headers.end()) {
          EXPECT_TRUE(VerifyEs256Jwt(resp_iter->second));
        }
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));
  ASSERT_TRUE(server_.Start());
  TestRegistrationCallback callback;
  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
}

TEST_F(RegistrationTest, RefreshWithNewSessionIdFails) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), "old_session_id", kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();

  const RegistrationResult& out_session = callback.outcome();
  SessionError::ErrorType session_error =
      out_session.SessionErrorForTesting()->type;
  EXPECT_EQ(session_error, SessionError::kMismatchedSessionId);
}

TEST_F(RegistrationTest, RegistrationWithNonStringRefreshInitiatorsFails) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  constexpr char kNonStringInitiator[] =
      R"({
  "session_identifier": "session_id",
  "scope": {
    "include_site": true,
    "scope_specification" : [
      {
        "type": "include",
        "domain": "trusted.example.com",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "auth_cookie",
    "attributes": "Domain=example.com; Path=/; Secure; SameSite=None"
  }],
  "allowed_refresh_initiators": [ 12345 ]
})";
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kNonStringInitiator));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto isolation_info = IsolationInfo::CreateTransient(/*nonce=*/std::nullopt);
  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(), isolation_info, isolation_info.site_for_cookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();

  const RegistrationResult& out_session = callback.outcome();
  SessionError::ErrorType session_error =
      out_session.SessionErrorForTesting()->type;
  EXPECT_EQ(session_error, SessionError::kRefreshInitiatorNotString);
}

TEST_F(RegistrationTest, MissingIncludeSiteFails) {
  constexpr char kTestingJson[] =
      R"({
  "session_identifier": "session_id",
  "refresh_url": "/refresh",
  "scope": {
    "scope_specification" : [
      {
        "type": "include",
        "domain": "trusted.a.test"
      },
      {
        "type": "exclude",
        "domain": "new.a.test",
        "path": "/only_trusted_path"
      }
    ]
  },
  "credentials": [{
    "type": "cookie",
    "name": "other_cookie",
    "attributes": "Domain=a.test; Path=/; Secure; SameSite=None"
  }]
})";

  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kTestingJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kMissingScopeIncludeSite);
}

TEST_F(RegistrationTest, ShutdownDuringRequest) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  base::RunLoop run_loop;
  server_.RegisterRequestHandler(base::BindRepeating(
      [](base::RunLoop* run_loop, const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        run_loop->Quit();
        return std::make_unique<test_server::HungResponse>();
      },
      &run_loop));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());

  run_loop.Run();

  EXPECT_EQ(context_->url_requests()->size(), 1u);

  fetcher.reset();

  EXPECT_EQ(context_->url_requests()->size(), 0u);
}

TEST_F(RegistrationTest, EmptyResponseOnRegistration) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, ""));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kEmptySessionConfig);
}

TEST_F(RegistrationTest, EmptyResponseOnRefresh) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, ""));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto request_param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  UnexportableSigningKeyId key = CreateSigningKey();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          request_param, session_service(), unexportable_key_service(),
          context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartFetchWithExistingKey(request_param, std::move(key),
                                     callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  out_session.NoSessionConfigChangeForTesting();
}

TEST_F(RegistrationTest, SetChallengeOnRegistration) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating([](const test_server::HttpRequest& request)
                              -> std::unique_ptr<test_server::HttpResponse> {
        auto response = std::make_unique<test_server::BasicHttpResponse>();
        response->set_code(HTTP_OK);
        response->AddCustomHeader(kSessionChallengeHeaderName,
                                  R"("test_challenge";id="session_id")");
        response->set_content_type("application/json");
        response->set_content(kBasicValidJson);
        return response;
      }));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam();
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionForTesting().cached_challenge(),
            "test_challenge");
}

TEST_F(RegistrationTest, RegistrationBySubdomain_Success) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "registering_origins": [ "https://subdomain.a.test:$1" ]
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("subdomain.a.test", "/");

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, RegistrationBySubdomain_WellKnownUnavailable) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, "")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("subdomain.a.test", "/");

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kSubdomainRegistrationWellKnownUnavailable);
}

TEST_F(RegistrationTest, RegistrationBySubdomain_WellKnownMalformed) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnResponse, HTTP_OK, "invalid JSON")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("subdomain.a.test", "/");

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kSubdomainRegistrationWellKnownMalformed);
}

TEST_F(RegistrationTest, RegistrationBySubdomain_WellKnownMalformedEntry) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnResponse, HTTP_OK,
                          "{\"registering_origins\": [ 12345 ]}")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("subdomain.a.test", "/");

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kSubdomainRegistrationWellKnownMalformed);
}

TEST_F(RegistrationTest, RegistrationBySubdomain_Unauthorized) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "registering_origins": [ "https://subdomain.a.test:$1" ]
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("not-allowed-subdomain.a.test", "/");

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kSubdomainRegistrationUnauthorized);
}

TEST_F(RegistrationTest, RegistrationBySubdomain_IncludeSiteFalse_Fails) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnResponse, HTTP_OK, kSubdomainValidJsonIncludeSiteFalse));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("subdomain.a.test", "/");

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  ASSERT_NE(out_session.SessionErrorForTesting(), nullptr);
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kCrossOriginRegistrationSiteNotIncluded);
}

TEST_F(RegistrationTest, RegistrationBySubdomain_MultipleAllowed) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "registering_origins": [
                              "https://subdomain.a.test:$1",
                              "https://other-subdomain.a.test:$1"
                            ]
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("subdomain.a.test", "/");
  auto param = GetBasicParam(registration_url);

  {
    TestRegistrationCallback callback;
    std::unique_ptr<RegistrationFetcher> fetcher =
        RegistrationFetcher::CreateFetcher(
            param, session_service(), unexportable_key_service(),
            context_.get(),
            IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
            SiteForCookies(),
            /*net_log_source=*/std::nullopt,
            /*original_request_initiator=*/std::nullopt,
            unexportable_keys::BackgroundTaskPriority::kBestEffort);
    fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                      callback.callback());

    callback.WaitForCall();
    const RegistrationResult& out_session = callback.outcome();
    out_session.SessionForTesting();
  }

  {
    TestRegistrationCallback callback;
    registration_url = server_.GetURL("other-subdomain.a.test", "/");

    param = GetBasicParam(registration_url);

    std::unique_ptr<RegistrationFetcher> fetcher =
        RegistrationFetcher::CreateFetcher(
            param, session_service(), unexportable_key_service(),
            context_.get(),
            IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
            SiteForCookies(),
            /*net_log_source=*/std::nullopt,
            /*original_request_initiator=*/std::nullopt,
            unexportable_keys::BackgroundTaskPriority::kBestEffort);
    fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                      callback.callback());
    callback.WaitForCall();
    callback.outcome().SessionForTesting();
  }
}

TEST_F(RegistrationTest, RegistrationRedirectToSubdomain) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  bool well_known_fetched = false;

  // 1. Redirect request
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        if (request.relative_url != "/") {
          return nullptr;
        }
        auto response = std::make_unique<test_server::BasicHttpResponse>();
        response->set_code(HTTP_FOUND);
        response->AddCustomHeader(
            "Location", server_.GetURL("subdomain.a.test", "/dbsc").spec());
        return response;
      }));

  // 2. Return config
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        if (request.relative_url != "/dbsc") {
          return nullptr;
        }
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));

  // 3. Monitor well-known requests
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        if (request.relative_url != "/.well-known/device-bound-sessions") {
          return nullptr;
        }
        well_known_fetched = true;
        return ReturnResponse(HTTP_NOT_FOUND, "", request);
      }));

  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("a.test", "/");
  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  // Verify well-known check is triggered and registration fails.
  EXPECT_TRUE(well_known_fetched);
  EXPECT_EQ(callback.outcome().SessionErrorForTesting()->type,
            SessionError::kSubdomainRegistrationWellKnownUnavailable);
}

TEST_F(RegistrationTest, FederatedWellKnownDiscoverySendsFetchMetadata) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  test_server::HttpRequest::HeaderMap captured_headers;
  bool well_known_fetched = false;

  // 1. Initial Endpoint Response: Instruct the Fetcher to redirect onto a
  // Subdomain scope, implicitly triggering the DBSC cross-origin .well-known
  // verification pipeline.
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        if (request.relative_url != "/") {
          return nullptr;
        }
        auto response = std::make_unique<test_server::BasicHttpResponse>();
        response->set_code(HTTP_FOUND);
        response->AddCustomHeader(
            "Location", server_.GetURL("subdomain.a.test", "/dbsc").spec());
        return response;
      }));

  // 2. Mock Config Delivery for the Subdomain path.
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        if (request.relative_url != "/dbsc") {
          return nullptr;
        }
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));

  // 3. .well-known Interceptor: Capture the outbound GET metadata for the
  // cross-origin validation check.
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const test_server::HttpRequest& request)
          -> std::unique_ptr<test_server::HttpResponse> {
        if (request.relative_url != "/.well-known/device-bound-sessions") {
          return nullptr;
        }

        for (const char* name :
             {"Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"}) {
          auto it = request.headers.find(name);
          if (it != request.headers.end()) {
            captured_headers[name] = it->second;
          }
        }

        well_known_fetched = true;
        // Yield a 404 to cleanly isolate the test strictly to the pre-flight
        // discovery dispatch.
        return ReturnResponse(HTTP_NOT_FOUND, "", request);
      }));

  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("a.test", "/");
  TestRegistrationCallback callback;

  // Trigger Subdomain Cross-Origin Registration, implicitly firing the
  // .well-known discovery step on the server-bound origin.
  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          net::SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  // 1. Assert the .well-known validator was indeed engaged by the DBSC
  // state-machine.
  EXPECT_TRUE(well_known_fetched);

  // 2. Validate that our ConfigureWellKnownRequest pipeline successfully
  // computed and appended W3C 'no-cors', 'empty', and Origin-relative Metadata.
  EXPECT_THAT(captured_headers, Contains(Pair("Sec-Fetch-Mode", "no-cors")));
  EXPECT_THAT(captured_headers, Contains(Pair("Sec-Fetch-Dest", "empty")));
  EXPECT_THAT(captured_headers,
              Contains(Pair("Sec-Fetch-Site", "same-origin")));
}

TEST_F(RegistrationTest, FederatedSuccess) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [ "https://rp.a.test:$1" ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1"
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
  EXPECT_EQ(session_or_error.SessionForTesting().unexportable_key_id(), key);
}

TEST_F(RegistrationTest, FederatedProviderHasProvider) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "provider_origin": "https://provider-provider.a.test:$1",
                                                "relying_origins": [ "https://rp.a.test:$1" ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1",
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));

  EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
            SessionError::kSessionProviderWellKnownHasProviderOrigin);
}

TEST_F(RegistrationTest, FederatedProviderUnvailable) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "provider.a.test",
      "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, "")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1",
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));

  EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
            SessionError::kSessionProviderWellKnownUnavailable);
}

TEST_F(RegistrationTest, FederatedProviderUnauthorized) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [ "https://other-rp.a.test:$1" ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1"
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));

  EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
            SessionError::kFederatedNotAuthorizedByProvider);
}

TEST_F(RegistrationTest, FederatedRelyingUnavailable) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [ "https://rp.a.test:$1" ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnResponse, HTTP_BAD_REQUEST, "")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));

  EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
            SessionError::kRelyingPartyWellKnownUnavailable);
}

TEST_F(RegistrationTest, FederatedRelyingHasRelying) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [ "https://rp.a.test:$1" ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1",
                            "relying_origins": [ "https://rp-rp.a.test:$1" ]
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));

  EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
            SessionError::kRelyingPartyWellKnownHasRelyingOrigins);
}

TEST_F(RegistrationTest, FederatedRelyingNotAuthorized) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [ "https://rp.a.test:$1" ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://other-provider.a.test:$1"
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));

  EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
            SessionError::kFederatedNotAuthorizedByRelyingParty);
}

TEST_F(RegistrationTest, FederatedTooManyRelying) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [
                                                  "https://rp.b1.test:$1",
                                                  "https://rp.b2.test:$1",
                                                  "https://rp.b3.test:$1",
                                                  "https://rp.b4.test:$1",
                                                  "https://rp.b5.test:$1",
                                                  "https://rp.a.test:$1"
                                                ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1"
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
  EXPECT_EQ(session_or_error.SessionErrorForTesting()->type,
            SessionError::kTooManyRelyingOriginLabels);
}

TEST_F(RegistrationTest, FederatedTooManyRelyingFirstLabelAllowed) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [
                                                  "https://a-is-allowed-because-its-first.a.test:$1",
                                                  "https://rp.b1.test:$1",
                                                  "https://rp.b2.test:$1",
                                                  "https://rp.b3.test:$1",
                                                  "https://rp.b4.test:$1",
                                                  "https://rp.b5.test:$1",
                                                  "https://rp.a.test:$1"
                                                ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1"
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
  EXPECT_EQ(session_or_error.SessionForTesting().unexportable_key_id(), key);
}

TEST_F(RegistrationTest, FederatedNotRegistrableDoesNotCount) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnForHostAndPath, "provider.a.test",
                          "/.well-known/device-bound-sessions",
                          base::BindRepeating(&ReturnWellKnown,
                                              R"json({
                                                "relying_origins": [
                                                  "https://tld",
                                                  "http://?not-a=url",
                                                  "http:///path",
                                                  "http:///path2",
                                                  "http:///path3",
                                                  "https://rp.a.test:$1"
                                                ]
                                              })json")));
  server_.RegisterRequestHandler(base::BindRepeating(
      &ReturnForHostAndPath, "rp.a.test", "/.well-known/device-bound-sessions",
      base::BindRepeating(&ReturnWellKnown,
                          R"json({
                            "provider_origin": "https://provider.a.test:$1"
                          })json")));
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  UnexportableSigningKeyId key = CreateSigningKey();
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("rp.a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);
  auto session_or_error =
      FetchWithFederatedKey(param, key, server_.GetURL("provider.a.test", "/"));
  EXPECT_EQ(session_or_error.SessionForTesting().unexportable_key_id(), key);
}

TEST_F(RegistrationTest, RegistrationFailsIfCantSetCookies) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  GURL registration_url = server_.GetURL("a.test", "/");

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = GetBasicParam(registration_url);
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);

  network_delegate()->set_cookie_options(TestNetworkDelegate::NO_SET_COOKIE);

  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();
  const RegistrationResult& out_session = callback.outcome();
  EXPECT_EQ(out_session.SessionErrorForTesting()->type,
            SessionError::kBoundCookieSetForbidden);
}

TEST_F(RegistrationTest, RegisterAuthorizationNoChallenge) {
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  server_.RegisterRequestHandler(
      base::BindRepeating(&ReturnResponse, HTTP_OK, kBasicValidJson));
  ASSERT_TRUE(server_.Start());

  RecordingNetLogObserver net_log_observer;
  TestRegistrationCallback callback;

  auto param = RegistrationRequestParam::CreateForTesting(
      GetBaseURL(), /*session_identifier=*/std::nullopt,
      /*challenge=*/std::nullopt, "authorization");
  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);
  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  // Validate the result is a session instead of an error.
  callback.outcome().SessionForTesting();
}

TEST_F(RegistrationTest, RefreshRetryTransientError) {
  base::test::ScopedFeatureList scoped_feature_list(
      features::kDeviceBoundSessionsRetryTransientRefreshErrors);
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  int request_count = 0;
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&request_count](const test_server::HttpRequest& request) {
        request_count++;
        if (request_count == 1) {
          // Trigger net error on first attempt (invalid raw response)
          return ReturnInvalidResponse(request);
        }
        // Success on the second attempt (the retry)
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);

  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);

  fetcher->StartFetchWithExistingKey(param, CreateSigningKey(),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_THAT(callback.outcome(), testing::Not(IsErrorRegistrationResult()));
  EXPECT_EQ(request_count, 2);

  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Refresh.Network.Result.FirstAttempt",
      net::ERR_INVALID_HTTP_RESPONSE, 1);
  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Refresh.Network.Result.SecondAttempt", 200, 1);
}

TEST_F(RegistrationTest, RefreshRetryTransientErrorOnSecondRoundtrip) {
  base::test::ScopedFeatureList scoped_feature_list(
      features::kDeviceBoundSessionsRetryTransientRefreshErrors);
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  int request_count = 0;
  int jwt_request_count = 0;
  server_.RegisterRequestHandler(
      base::BindLambdaForTesting([&request_count, &jwt_request_count](
                                     const test_server::HttpRequest& request) {
        request_count++;
        if (!request.headers.contains(kSessionResponseHeaderName)) {
          return ReturnChallengeResponse(HTTP_FORBIDDEN, "new_challenge");
        } else {
          jwt_request_count++;
          if (jwt_request_count == 1) {
            return ReturnInvalidResponse(request);
          }
          return ReturnResponse(HTTP_OK, kBasicValidJson, request);
        }
      }));
  ASSERT_TRUE(server_.Start());

  std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
  EXPECT_EQ(session->cached_challenge(), std::nullopt);

  EXPECT_CALL(
      session_service(),
      GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
      .WillRepeatedly(Return(session.get()));

  EXPECT_CALL(session_service(), HandleResponseHeaders)
      .WillRepeatedly([&session](DbscRequest& request,
                                 HttpResponseHeaders* headers,
                                 const FirstPartySetMetadata& fp) {
        session->set_cached_challenge("new_challenge");
      });

  EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge)
      .WillRepeatedly(Return(nullptr));
  EXPECT_CALL(session_service(), AddSigningOccurrence).Times(1);
  EXPECT_CALL(session_service(), SetLatestSignedRefreshChallenge).Times(1);
  EXPECT_CALL(session_service(), SigningQuotaExceeded)
      .WillRepeatedly(Return(false));

  TestRegistrationCallback callback;
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("a.test", "/"), kSessionIdentifier,
      /*challenge=*/std::nullopt,
      /*authorization=*/std::nullopt);

  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);

  fetcher->StartFetchWithExistingKey(param, CreateSigningKey(),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_THAT(callback.outcome(), Not(IsErrorRegistrationResult()));
  EXPECT_EQ(request_count, 3);

  EXPECT_THAT(
      histogram_tester.GetAllSamples(
          "Net.DeviceBoundSessions.Refresh.Network.Result.FirstAttempt"),
      BucketsAre(Bucket(403, 1), Bucket(net::ERR_INVALID_HTTP_RESPONSE, 1)));
  EXPECT_THAT(
      histogram_tester.GetAllSamples(
          "Net.DeviceBoundSessions.Refresh.Network.Result.SecondAttempt"),
      BucketsAre(Bucket(200, 1)));
}

TEST_F(RegistrationTest, RefreshRetryTransientErrorOnBothRoundtrips) {
  base::test::ScopedFeatureList scoped_feature_list(
      features::kDeviceBoundSessionsRetryTransientRefreshErrors);
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  int no_jwt_request_count = 0;
  int jwt_request_count = 0;
  server_.RegisterRequestHandler(
      base::BindLambdaForTesting([&no_jwt_request_count, &jwt_request_count](
                                     const test_server::HttpRequest& request) {
        if (!request.headers.contains(kSessionResponseHeaderName)) {
          no_jwt_request_count++;
          if (no_jwt_request_count == 1) {
            return ReturnInvalidResponse(request);
          }
          return ReturnChallengeResponse(HTTP_FORBIDDEN, "new_challenge");
        } else {
          jwt_request_count++;
          if (jwt_request_count == 1) {
            return ReturnInvalidResponse(request);
          }
          return ReturnResponse(HTTP_OK, kBasicValidJson, request);
        }
      }));
  ASSERT_TRUE(server_.Start());

  std::unique_ptr<Session> session = CreateTestSession(kSessionIdentifier);
  EXPECT_EQ(session->cached_challenge(), std::nullopt);

  EXPECT_CALL(
      session_service(),
      GetSession(SessionKey{SchemefulSite(GetBaseURL()), session->id()}))
      .WillRepeatedly(Return(session.get()));

  EXPECT_CALL(session_service(), HandleResponseHeaders)
      .WillRepeatedly([&session](DbscRequest& request,
                                 HttpResponseHeaders* headers,
                                 const FirstPartySetMetadata& fp) {
        session->set_cached_challenge("new_challenge");
      });

  EXPECT_CALL(session_service(), GetLatestSignedRefreshChallenge)
      .WillRepeatedly(Return(nullptr));
  EXPECT_CALL(session_service(), AddSigningOccurrence).Times(1);
  EXPECT_CALL(session_service(), SetLatestSignedRefreshChallenge).Times(1);
  EXPECT_CALL(session_service(), SigningQuotaExceeded)
      .WillRepeatedly(Return(false));

  TestRegistrationCallback callback;
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("a.test", "/"), kSessionIdentifier,
      /*challenge=*/std::nullopt,
      /*authorization=*/std::nullopt);

  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);

  fetcher->StartFetchWithExistingKey(param, CreateSigningKey(),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_THAT(callback.outcome(), Not(IsErrorRegistrationResult()));
  EXPECT_EQ(no_jwt_request_count, 2);
  EXPECT_EQ(jwt_request_count, 2);

  EXPECT_THAT(
      histogram_tester.GetAllSamples(
          "Net.DeviceBoundSessions.Refresh.Network.Result.FirstAttempt"),
      BucketsAre(Bucket(net::ERR_INVALID_HTTP_RESPONSE, 2)));
  EXPECT_THAT(
      histogram_tester.GetAllSamples(
          "Net.DeviceBoundSessions.Refresh.Network.Result.SecondAttempt"),
      BucketsAre(Bucket(403, 1), Bucket(200, 1)));
}

TEST_F(RegistrationTest, RefreshRetryTransientErrorDisabled) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndDisableFeature(
      features::kDeviceBoundSessionsRetryTransientRefreshErrors);
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  int request_count = 0;
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&request_count](const test_server::HttpRequest& request) {
        request_count++;
        if (request_count == 1) {
          // Trigger net error on first attempt (invalid raw response)
          return ReturnInvalidResponse(request);
        }
        // Success on the second attempt (the retry)
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("a.test", "/"), kSessionIdentifier, kChallenge,
      /*authorization=*/std::nullopt);

  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);

  fetcher->StartFetchWithExistingKey(param, CreateSigningKey(),
                                     callback.callback());
  callback.WaitForCall();

  EXPECT_THAT(callback.outcome(), IsErrorRegistrationResult());
  EXPECT_EQ(request_count, 1);

  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Refresh.Network.Result.FirstAttempt",
      net::ERR_INVALID_HTTP_RESPONSE, 1);
  histogram_tester.ExpectTotalCount(
      "Net.DeviceBoundSessions.Refresh.Network.Result.SecondAttempt", 0);
}

TEST_F(RegistrationTest, RegistrationNoRetryTransientError) {
  base::test::ScopedFeatureList scoped_feature_list(
      features::kDeviceBoundSessionsRetryTransientRefreshErrors);
  base::HistogramTester histogram_tester;
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;

  int request_count = 0;
  server_.RegisterRequestHandler(base::BindLambdaForTesting(
      [&request_count](const test_server::HttpRequest& request) {
        request_count++;
        if (request_count == 1) {
          return ReturnInvalidResponse(request);
        }
        return ReturnResponse(HTTP_OK, kBasicValidJson, request);
      }));
  ASSERT_TRUE(server_.Start());

  TestRegistrationCallback callback;
  auto param = RegistrationRequestParam::CreateForTesting(
      server_.GetURL("a.test", "/"), /*session_identifier=*/std::nullopt,
      kChallenge, /*authorization=*/std::nullopt);

  std::unique_ptr<RegistrationFetcher> fetcher =
      RegistrationFetcher::CreateFetcher(
          param, session_service(), unexportable_key_service(), context_.get(),
          IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
          SiteForCookies(),
          /*net_log_source=*/std::nullopt,
          /*original_request_initiator=*/std::nullopt,
          unexportable_keys::BackgroundTaskPriority::kBestEffort);

  fetcher->StartCreateTokenAndFetch(param, CreateAlgArray(),
                                    callback.callback());
  callback.WaitForCall();

  EXPECT_THAT(callback.outcome(), IsErrorRegistrationResult());
  EXPECT_EQ(request_count, 1);

  histogram_tester.ExpectUniqueSample(
      "Net.DeviceBoundSessions.Registration.Network.Result",
      net::ERR_INVALID_HTTP_RESPONSE, 1);
  histogram_tester.ExpectTotalCount(
      "Net.DeviceBoundSessions.Refresh.Network.Result.FirstAttempt", 0);
}

class RegistrationTokenHelperTest : public testing::Test {
 public:
  unexportable_keys::UnexportableKeyService& unexportable_key_service() {
    return unexportable_key_service_;
  }

  void RunBackgroundTasks() { task_environment_.RunUntilIdle(); }

 private:
  base::test::TaskEnvironment task_environment_{
      // QUEUED - tasks don't run until `RunUntilIdle()` is called.
      base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
  unexportable_keys::UnexportableKeyTaskManager task_manager_;
  unexportable_keys::UnexportableKeyServiceImpl unexportable_key_service_{
      task_manager_, kTaskOrigin, crypto::UnexportableKeyProvider::Config()};
};

TEST_F(RegistrationTokenHelperTest, CreateSuccess) {
  crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider;
  base::test::TestFuture<SessionErrorOr<RegistrationFetcher::RegistrationToken>>
      future;
  RegistrationFetcher::CreateRegistrationTokenAsyncForTesting(
      unexportable_key_service(), "test_challenge",
      /*authorization=*/std::nullopt, future.GetCallback());
  RunBackgroundTasks();
  ASSERT_TRUE(future.Get().has_value());
}

TEST_F(RegistrationTokenHelperTest, CreateFail) {
  crypto::ScopedNullUnexportableKeyProvider scoped_null_key_provider;
  base::test::TestFuture<SessionErrorOr<RegistrationFetcher::RegistrationToken>>
      future;
  RegistrationFetcher::CreateRegistrationTokenAsyncForTesting(
      unexportable_key_service(), "test_challenge",
      /*authorization=*/std::nullopt, future.GetCallback());
  RunBackgroundTasks();
  EXPECT_FALSE(future.Get().has_value());
}

}  // namespace

}  // namespace net::device_bound_sessions
