910e62b5创建于 1月15日历史提交
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/network/trust_token_browsertest.h"

#include <memory>
#include <string>
#include <string_view>

#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "build/build_config.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/trust_token_http_headers.h"
#include "services/network/public/cpp/trust_token_parameterization.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "services/network/test/trust_token_request_handler.h"
#include "services/network/test/trust_token_test_server_handler_registration.h"
#include "services/network/test/trust_token_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_canon_stdstring.h"

namespace content {

namespace {

using network::test::TrustTokenRequestHandler;
using SignedRequest = network::test::TrustTokenSignedRequest;
using ::testing::AllOf;
using ::testing::DescribeMatcher;
using ::testing::Eq;
using ::testing::ExplainMatchResult;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::IsFalse;
using ::testing::IsSubsetOf;
using ::testing::Not;
using ::testing::Optional;
using ::testing::StrEq;
using ::testing::Truly;

MATCHER_P(HasHeader, name, base::StringPrintf("Has header %s", name)) {
  if (!arg.headers.HasHeader(name)) {
    *result_listener << base::StringPrintf("%s wasn't present", name);
    return false;
  }
  *result_listener << base::StringPrintf("%s was present", name);
  return true;
}

MATCHER_P2(HasHeader,
           name,
           other_matcher,
           "has header " + std::string(name) + " that " +
               DescribeMatcher<std::string>(other_matcher)) {
  std::optional<std::string> header = arg.headers.GetHeader(name);
  if (!header) {
    *result_listener << base::StringPrintf("%s wasn't present", name);
    return false;
  }
  return ExplainMatchResult(other_matcher, *header, result_listener);
}

MATCHER(
    ReflectsSigningFailure,
    "The given signed request reflects a client-side signing failure, having "
    "an empty redemption record and no other related headers.") {
  return ExplainMatchResult(
      AllOf(HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord,
                      StrEq("")),
            Not(HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))),
      arg, result_listener);
}

}  // namespace

TrustTokenBrowsertest::TrustTokenBrowsertest() = default;

void TrustTokenBrowsertest::SetUpOnMainThread() {
  host_resolver()->AddRule("*", "127.0.0.1");

  server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
  server_.AddDefaultHandlers(
      base::FilePath(FILE_PATH_LITERAL("content/test/data")));

  SetupCrossSiteRedirector(embedded_test_server());
  SetupCrossSiteRedirector(&server_);

  network::test::RegisterTrustTokenTestHandlers(&server_, &request_handler_);

  TrustTokenBrowsertest::Observe(shell()->web_contents());

  ASSERT_TRUE(server_.Start());
}

void TrustTokenBrowsertest::ProvideRequestHandlerKeyCommitmentsToNetworkService(
    std::vector<std::string_view> hosts) {
  base::flat_map<url::Origin, std::string_view> origins_and_commitments;
  std::string key_commitments = request_handler_.GetKeyCommitmentRecord();

  // TODO(davidvc): This could be extended to make the request handler aware
  // of different origins, which would allow using different key commitments
  // per origin.
  for (std::string_view host : hosts) {
    GURL::Replacements replacements;
    replacements.SetHostStr(host);
    origins_and_commitments.insert_or_assign(
        url::Origin::Create(server_.base_url().ReplaceComponents(replacements)),
        key_commitments);
  }

  if (origins_and_commitments.empty()) {
    origins_and_commitments = {
        {url::Origin::Create(server_.base_url()), key_commitments}};
  }

  base::RunLoop run_loop;
  GetNetworkService()->SetTrustTokenKeyCommitments(
      network::WrapKeyCommitmentsForIssuers(std::move(origins_and_commitments)),
      run_loop.QuitClosure());
  run_loop.Run();
}

std::string TrustTokenBrowsertest::IssuanceOriginFromHost(
    const std::string& host) const {
  auto ret = url::Origin::Create(server_.GetURL(host, "/")).Serialize();
  return ret;
}

void TrustTokenBrowsertest::OnTrustTokensAccessed(
    RenderFrameHost* render_frame_host,
    const TrustTokenAccessDetails& details) {
  access_count_++;
}

void TrustTokenBrowsertest::OnTrustTokensAccessed(
    NavigationHandle* navigation_handle,
    const TrustTokenAccessDetails& details) {
  access_count_++;
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEnd) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(
  (async () => {
    await fetch("/issue", {privateToken: {version: 1,
                                        operation: 'token-request'}});
    await fetch("/redeem", {privateToken: {version: 1,
                                         operation: 'token-redemption'}});
    await fetch("/sign", {privateToken: {version: 1,
                                       operation: 'send-redemption-record',
                                       issuers: [$1]}});
    return "Success"; })(); )";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ(
      "Success",
      EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test"))));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // Expect three accesses, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

// Fetch is called directly from top level (a.test), issuer origin (b.test)
// is different from top frame origin.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEndThirdParty) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"b.test"});

  const GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(
  (async () => {
    await fetch($1, {privateToken: {version: 1,
                                        operation: 'token-request'}});
    await fetch($2, {privateToken: {version: 1,
                                         operation: 'token-redemption'}});
    await fetch($3, {privateToken: {version: 1,
                                       operation: 'send-redemption-record',
                                       issuers: [$4]}});
    return "Success"; })(); )";

  const std::string issuer_origin = IssuanceOriginFromHost("b.test");
  const std::string issuance_url = server_.GetURL("b.test", "/issue").spec();
  const std::string redemption_url = server_.GetURL("b.test", "/redeem").spec();
  const std::string signature_url = server_.GetURL("b.test", "/sign").spec();

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(command, issuance_url, redemption_url,
                                      signature_url, issuer_origin)));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // Expect three accesses, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, XhrEndToEnd) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  // If this isn't idiomatic JS, I don't know what is.
  std::string command = R"(
  (async () => {
    let request = new XMLHttpRequest();
    request.open('GET', '/issue');
    request.setPrivateToken({
      version: 1,
      operation: 'token-request'
    });
    let promise = new Promise((res, rej) => {
      request.onload = res; request.onerror = rej;
    });
    request.send();
    await promise;

    request = new XMLHttpRequest();
    request.open('GET', '/redeem');
    request.setPrivateToken({
      version: 1,
      operation: 'token-redemption'
    });
    promise = new Promise((res, rej) => {
      request.onload = res; request.onerror = rej;
    });
    request.send();
    await promise;

    request = new XMLHttpRequest();
    request.open('GET', '/sign');
    request.setPrivateToken({
      version: 1,
      operation: 'send-redemption-record',
      issuers: [$1]
    });
    promise = new Promise((res, rej) => {
      request.onload = res; request.onerror = rej;
    });
    request.send();
    await promise;
    return "Success";
    })(); )";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ(
      "Success",
      EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test"))));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // Expect three accesses, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, IframeSendRedemptionRecord) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  std::string command = R"(
  (async () => {
    await fetch("/issue", {privateToken: {version: 1,
                                        operation: 'token-request'}});
    await fetch("/redeem", {privateToken: {version: 1,
                                         operation: 'token-redemption'}});
    return "Success";
  })())";

  GURL start_url = server_.GetURL("a.test", "/page_with_iframe.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  EXPECT_EQ("Success", EvalJs(shell(), command));

  auto execute_op_via_iframe = [&](std::string_view path,
                                   std::string_view trust_token) {
    // It's important to set the trust token arguments before updating src, as
    // the latter triggers a load.
    EXPECT_TRUE(ExecJs(
        shell(), JsReplace(
                     R"( const myFrame = document.getElementById("test_iframe");
                         myFrame.privateToken = $1;
                         myFrame.src = $2;)",
                     trust_token, path)));
    TestNavigationObserver load_observer(shell()->web_contents());
    load_observer.WaitForNavigationFinished();
  };

  execute_op_via_iframe("/sign", JsReplace(
                                     R"({"version": 1,
              "operation": "send-redemption-record",
              "issuers": [$1]})",
                                     IssuanceOriginFromHost("a.test")));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // Expect three accesses, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       IframeCanOnlySendRedemptionRecord) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/page_with_iframe.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  auto fail_to_execute_op_via_iframe = [&](std::string_view path,
                                           std::string_view trust_token) {
    // It's important to set the trust token arguments before updating src, as
    // the latter triggers a load.
    EXPECT_TRUE(ExecJs(
        shell(), JsReplace(
                     R"( const myFrame = document.getElementById("test_iframe");
                         myFrame.trustToken = $1;
                         myFrame.src = $2;)",
                     trust_token, path)));
    TestNavigationObserver load_observer(shell()->web_contents());
    load_observer.WaitForNavigationFinished();
  };

  fail_to_execute_op_via_iframe("/issue", R"({"type": "token-request"})");
  std::string command = JsReplace(R"(
  (async () => {
    return await document.hasPrivateToken($1);
  })();)",
                                  IssuanceOriginFromHost("a.test"));

  EXPECT_EQ(false, EvalJs(shell(), command));

  fail_to_execute_op_via_iframe("/redeem", R"({"type": "token-redemption"})");
  command = JsReplace(R"(
  (async () => {
    return document.hasRedemptionRecord($1);
  })();)",
                      IssuanceOriginFromHost("a.test"));
  EXPECT_EQ(false, EvalJs(shell(), command));

  fail_to_execute_op_via_iframe("/bad", R"({"type": "bad-type"})");
  command = JsReplace(R"(
  (async () => {
    return await document.hasPrivateToken($1)
    || document.hasRedemptionRecord($1);
  })();)",
                      IssuanceOriginFromHost("a.test"));
  EXPECT_EQ(false, EvalJs(shell(), command));

  // Expect zero accesses.
  EXPECT_EQ(0, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, HasTrustTokenAfterIssuance) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = JsReplace(R"(
  (async () => {
    await fetch("/issue", {privateToken: {version: 1,
                                        operation: 'token-request'}});
    return await document.hasPrivateToken($1);
  })();)",
                                  IssuanceOriginFromHost("a.test"));

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  //
  // Note: EvalJs's EXPECT_EQ type-conversion magic only supports the
  // "Yoda-style" EXPECT_EQ(expected, actual).
  EXPECT_EQ(true, EvalJs(shell(), command));

  // Expect one access for issue.
  EXPECT_EQ(1, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       SigningWithNoRedemptionRecordDoesntCancelRequest) {
  TrustTokenRequestHandler::Options options;
  request_handler_.UpdateOptions(std::move(options));

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  // This sign operation will fail, because we don't have a redemption record in
  // storage, a prerequisite. However, the failure shouldn't be fatal.
  std::string command = JsReplace(R"((async () => {
      await fetch("/sign", {privateToken: {version: 1,
                                         operation: 'send-redemption-record',
                                         issuers: [$1]}});
      return "Success";
      })(); )",
                                  IssuanceOriginFromHost("a.test"));

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ("Success", EvalJs(shell(), command));

  EXPECT_THAT(request_handler_.last_incoming_signed_request(),
              Optional(ReflectsSigningFailure()));

  // Expect one access for sign.
  EXPECT_EQ(1, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEndInIsolatedWorld) {
  // Ensure an isolated world can execute Trust Tokens operations when its
  // window's main world can. In particular, this ensures that the
  // redemtion-and-signing permissions policy is appropriately propagated by the
  // browser process.

  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(
  (async () => {
    await fetch("/issue", {privateToken: {version: 1,
                                        operation: 'token-request'}});
    await fetch("/redeem", {privateToken: {version: 1,
                                         operation: 'token-redemption'}});
    await fetch("/sign", {privateToken: {version: 1,
                                       operation: 'send-redemption-record',
                                  issuers: [$1]}});
    return "Success"; })(); )";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ(
      "Success",
      EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test")),
             EXECUTE_SCRIPT_DEFAULT_OPTIONS,
             /*world_id=*/30));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // Expect three accesses, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RecordsTimers) {
  base::HistogramTester histograms;

  // |completion_waiter| adds a synchronization point so that we can
  // safely fetch all of the relevant histograms from the network process.
  //
  // Without this, there's a race between the fetch() promises resolving and the
  // NetErrorForTrustTokenOperation histogram being logged. This likely has no
  // practical impact during normal operation, but it makes this test flake: see
  // https://crbug.com/1165862.
  //
  // The URLLoaderInterceptor's completion callback receives its
  // URLLoaderCompletionStatus from URLLoaderClient::OnComplete, which happens
  // after CorsURLLoader::NotifyCompleted, which records the final histogram.
  base::RunLoop run_loop;
  content::URLLoaderInterceptor completion_waiter(
      base::BindRepeating([](URLLoaderInterceptor::RequestParams*) {
        return false;  // Don't intercept outbound requests.
      }),
      base::BindLambdaForTesting(
          [&run_loop](const GURL& url,
                      const network::URLLoaderCompletionStatus& status) {
            if (url.spec().find("sign") != std::string::npos)
              run_loop.Quit();
          }),
      /*ready_callback=*/base::NullCallback());

  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(
  (async () => {
    await fetch("/issue", {privateToken: {version: 1,
                                        operation: 'token-request'}});
    await fetch("/redeem", {privateToken: {version: 1,
                                         operation: 'token-redemption'}});
    await fetch("/sign", {privateToken: {version: 1,
                                       operation: 'send-redemption-record',
                                  issuers: [$1]}});
    return "Success"; })(); )";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ(
      "Success",
      EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("a.test"))));

  run_loop.Run();
  content::FetchHistogramsFromChildProcesses();

  // Just check that the timers were populated: since we can't mock a clock in
  // this browser test, it's hard to check the recorded values for
  // reasonableness.
  for (const std::string& op : {"Issuance", "Redemption", "Signing"}) {
    histograms.ExpectTotalCount(
        "Net.TrustTokens.OperationBeginTime.Success." + op, 1);
    histograms.ExpectTotalCount(
        "Net.TrustTokens.OperationTotalTime.Success." + op, 1);
    histograms.ExpectTotalCount(
        "Net.TrustTokens.OperationServerTime.Success." + op, 1);
    histograms.ExpectTotalCount(
        "Net.TrustTokens.OperationFinalizeTime.Success." + op, 1);
    histograms.ExpectUniqueSample(
        "Net.TrustTokens.NetErrorForTrustTokenOperation.Success." + op, net::OK,
        1);
  }

  histograms.ExpectTotalCount("Net.TrustTokens.ProtocolVersion", 1);
  histograms.ExpectUniqueSample(
      "Net.TrustTokens.ProtocolVersion",
      network::mojom::TrustTokenProtocolVersion::kTrustTokenV3Pmb, 1);
  // Expect three accesses, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RecordsNetErrorCodes) {
  // Verify that the Net.TrustTokens.NetErrorForTrustTokenOperation.* metrics
  // record successfully by testing two "success" cases where there's an
  // unrelated net stack error and one case where the Trust Tokens operation
  // itself fails.
  base::HistogramTester histograms;

  ProvideRequestHandlerKeyCommitmentsToNetworkService(
      {"no-cert-for-this.domain"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  EXPECT_THAT(
      EvalJs(shell(), JsReplace(
                          R"(fetch($1, {privateToken: {
                                            version: 1,
                                            operation: 'token-request'}})
                   .then(() => "Unexpected success!")
                   .catch(err => err.message);)",
                          IssuanceOriginFromHost("no-cert-for-this.domain")))
          .ExtractString(),
      HasSubstr("Failed to fetch"));

  EXPECT_THAT(
      EvalJs(shell(), JsReplace(
                          R"(fetch($1, {privateToken: {
                                         version: 1,
                                         operation: 'send-redemption-record',
                 issuers: ['https://nonexistent-issuer.example']}})
                   .then(() => "Unexpected success!")
                   .catch(err => err.message);)",
                          IssuanceOriginFromHost("no-cert-for-this.domain")))
          .ExtractString(),
      HasSubstr("Failed to fetch"));

  content::FetchHistogramsFromChildProcesses();

  // "Success" since we executed the outbound half of the Trust Tokens
  // operation without issue:
  histograms.ExpectUniqueSample(
      "Net.TrustTokens.NetErrorForTrustTokenOperation.Success.Issuance",
      net::ERR_CERT_COMMON_NAME_INVALID, 1);

  // "Success" since signing can't fail:
  histograms.ExpectUniqueSample(
      "Net.TrustTokens.NetErrorForTrustTokenOperation.Success.Signing",
      net::ERR_CERT_COMMON_NAME_INVALID, 1);

  // Attempt a redemption against 'a.test'; we don't have a token for this
  // domain, so it should fail.
  EXPECT_EQ("InvalidStateError",
            EvalJs(shell(), JsReplace(
                                R"(fetch($1, {privateToken: {
                                            version: 1,
                                            operation: 'token-redemption'}})
                   .then(() => "Unexpected success!")
                   .catch(err => err.name);)",
                                IssuanceOriginFromHost("a.test"))));

  content::FetchHistogramsFromChildProcesses();

  histograms.ExpectUniqueSample(
      "Net.TrustTokens.NetErrorForTrustTokenOperation.Failure.Redemption",
      net::ERR_TRUST_TOKEN_OPERATION_FAILED, 1);

  // Expect three accesses, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RecordsFetchFailureReasons) {
  // Verify that the Net.TrustTokens.NetErrorForFetchFailure.* metrics
  // record successfully by testing one case with a blocked resource, one case
  // with a generic net-stack failure, and one case with a Trust Tokens
  // operation failure.
  base::HistogramTester histograms;

  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  // This fetch will fail because we set `redirect: 'error'` and the
  // /cross-site/ URL will redirect the request.
  EXPECT_EQ("TypeError", EvalJs(shell(),
                                R"(fetch("/cross-site/b.test/issue", {
                                     redirect: 'error',
                                     privateToken: {version: 1,
                                                  operation: 'token-request'}
                                   })
                                   .then(() => "Unexpected success!")
                                   .catch(err => err.name);)"));

  content::FetchHistogramsFromChildProcesses();
  histograms.ExpectUniqueSample(
      "Net.TrustTokens.NetErrorForFetchFailure.Issuance", net::ERR_FAILED,
      /*expected_count=*/1);

  // Since issuance failed, there should be no tokens to redeem, so redemption
  // should fail:
  EXPECT_EQ("OperationError", EvalJs(shell(),
                                     R"(fetch("/redeem", {privateToken: {
                                            version: 1,
                                            operation: 'token-redemption'}})
                   .then(() => "Unexpected success!")
                   .catch(err => err.name);)"));

  content::FetchHistogramsFromChildProcesses();
  histograms.ExpectUniqueSample(
      "Net.TrustTokens.NetErrorForFetchFailure.Redemption",
      net::ERR_TRUST_TOKEN_OPERATION_FAILED,
      /*expected_count=*/1);

  // Execute a cross-site b.test -> a.test issuance that would succeed, were it
  // not for site b requiring CORP headers and none being present on the a.test
  // issuance response:
  ASSERT_TRUE(NavigateToURL(
      shell(),
      server_.GetURL("b.test",
                     "/cross-origin-opener-policy_redirect_final.html")));
  GURL site_a_issuance_url =
      GURL(IssuanceOriginFromHost("a.test")).Resolve("/issue");
  EXPECT_THAT(EvalJs(shell(), JsReplace(R"(fetch($1, {
  mode: 'no-cors',
                  privateToken: {version: 1,
                               operation: 'token-request'}})
                   .then(() => "Unexpected success!")
                   .catch(err => err.message);)",
                                        site_a_issuance_url))
                  .ExtractString(),
              HasSubstr("Failed to fetch"));

  content::FetchHistogramsFromChildProcesses();
  histograms.ExpectBucketCount(
      "Net.TrustTokens.NetErrorForFetchFailure.Issuance",
      net::ERR_BLOCKED_BY_RESPONSE,
      /*expected_count=*/1);

  // Expect three accesses, two for issue and one for redeem.
  EXPECT_EQ(3, access_count_);
}

// Trust Tokens should require that their executing contexts be secure.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, OperationsRequireSecureContext) {
  ASSERT_TRUE(embedded_test_server()->Start());

  GURL start_url =
      embedded_test_server()->GetURL("insecure.test", "/page_with_iframe.html");
  // Make sure that we are, in fact, using an insecure page.
  ASSERT_FALSE(network::IsUrlPotentiallyTrustworthy(start_url));

  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  // 1. Confirm that the Fetch interface doesn't work:
  std::string command =
      R"(fetch("/issue", {privateToken: {version: 1,
                                       operation: 'token-request'}})
           .catch(error => error.message);)";
  EXPECT_THAT(EvalJs(shell(), command).ExtractString(),
              HasSubstr("secure context"));

  // 2. Confirm that the XHR interface isn't present:
  EXPECT_EQ(false, EvalJs(shell(), "'setTrustToken' in (new XMLHttpRequest);"));

  // 3. Confirm that the iframe interface doesn't work by verifying that no
  // Trust Tokens operation gets executed.
  GURL issuance_url = server_.GetURL("/issue");
  URLLoaderMonitor monitor({issuance_url});
  // It's important to set the trust token arguments before updating src, as
  // the latter triggers a load.
  EXPECT_TRUE(ExecJs(
      shell(), JsReplace(
                   R"( const myFrame = document.getElementById("test_iframe");
                       myFrame.trustToken = $1;
                       myFrame.src = $2;)",
                   R"({"operation": "token-request"})", issuance_url)));
  monitor.WaitForUrls();
  EXPECT_THAT(monitor.GetRequestInfo(issuance_url),
              Optional(Field(&network::ResourceRequest::trust_token_params,
                             IsFalse())));

  // Expect zero accesses.
  EXPECT_EQ(0, access_count_);
}

// Issuance should fail if we don't have keys for the issuer at hand.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, IssuanceRequiresKeys) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService(
      {"not-the-right-server.example"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(
    fetch('/issue', {privateToken: {version: 1,
                                  operation: 'token-request'}})
    .then(() => 'Success').catch(err => err.name); )";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ("InvalidStateError", EvalJs(shell(), command));

  // Expect one access of issue.
  EXPECT_EQ(1, access_count_);
}

// When the server rejects issuance, the client-side issuance operation should
// fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       CorrectlyReportsServerErrorDuringIssuance) {
  TrustTokenRequestHandler::Options options;
  options.issuance_outcome =
      TrustTokenRequestHandler::ServerOperationOutcome::kUnconditionalFailure;
  request_handler_.UpdateOptions(std::move(options));

  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  EXPECT_EQ("OperationError", EvalJs(shell(), R"(fetch('/issue',
        { privateToken: { version: 1, operation: 'token-request' } })
        .then(()=>'Success').catch(err => err.name); )"));

  // Expect one access of issue.
  EXPECT_EQ(1, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, CrossOriginIssuanceWorks) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"sub1.b.test"});

  GURL start_url = server_.GetURL("sub2.b.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  // Using GetURL to generate the issuance location is important
  // because it sets the port correctly.
  EXPECT_EQ(
      "Success",
      EvalJs(shell(), JsReplace(R"(
            fetch($1, { privateToken: { version: 1,
                                      operation: 'token-request' } })
            .then(()=>'Success'); )",
                                server_.GetURL("sub1.b.test", "/issue"))));

  // Expect one access of issue.
  EXPECT_EQ(1, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, CrossSiteIssuanceWorks) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("b.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  // Using GetURL to generate the issuance location is important
  // because it sets the port correctly.
  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(
            fetch($1, { privateToken: { version: 1,
                                      operation: 'token-request' } })
            .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/issue"))));

  // Expect one access of issue.
  EXPECT_EQ(1, access_count_);
}

// Issuance should succeed only if the number of issuers associated with the
// requesting context's top frame origin is less than the limit on the number of
// such issuers.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       IssuanceRespectsAssociatedIssuersCap) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  static_assert(
      network::kTrustTokenPerToplevelMaxNumberOfAssociatedIssuers < 10,
      "Consider rewriting this test for performance's sake if the "
      "number-of-issuers limit gets too large.");

  // Each hasPrivateStateToken call adds the provided issuer to the calling
  // context's list of associated issuers.
  for (int i = 0;
       i < network::kTrustTokenPerToplevelMaxNumberOfAssociatedIssuers; ++i) {
    ASSERT_EQ("Success", EvalJs(shell(), "document.hasPrivateToken('https://a" +
                                             base::NumberToString(i) +
                                             ".test').then(()=>'Success');"));
  }

  EXPECT_EQ("OperationError", EvalJs(shell(), R"(
            fetch('/issue', { privateToken: { version: 1,
                                            operation: 'token-request' } })
            .then(() => 'Success').catch(error => error.name); )"));

  // Expect one access for issue.
  EXPECT_EQ(1, access_count_);
}

// When an issuance request is made in cors mode, a cross-origin redirect from
// issuer A to issuer B should result in a new issuance request to issuer B,
// obtaining issuer B tokens on success.
//
// Note: For more on the interaction between Trust Tokens and redirects, see the
// "Handling redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
    TrustTokenBrowsertest,
    CorsModeCrossOriginRedirectIssuanceUsesNewOriginAsIssuer) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(fetch($1, {privateToken: {
                                          version: 1,
                                          operation: 'token-request'}})
                             .then(() => "Success")
                             .catch(error => error.name);)";

  EXPECT_EQ(
      "Success",
      EvalJs(shell(),
             JsReplace(command,
                       server_.GetURL("a.test", "/cross-site/b.test/issue"))));

  EXPECT_EQ(true, EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
                                            IssuanceOriginFromHost("b.test"))));
  EXPECT_EQ(false,
            EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
                                      IssuanceOriginFromHost("a.test"))));

  // Expect two accesses for issues.
  EXPECT_EQ(2, access_count_);
}

// When an issuance request is made in no-cors mode, a cross-origin redirect
// from issuer A to issuer B should result in recycling the original issuance
// request, obtaining issuer A tokens on success.
//
// Note: For more on the interaction between Trust Tokens and redirects, see the
// "Handling redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
    TrustTokenBrowsertest,
    NoCorsModeCrossOriginRedirectIssuanceUsesOriginalOriginAsIssuer) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(fetch($1, {mode: 'no-cors',
                                      privateToken: {
                                          version: 1,
                                          operation: 'token-request'}})
                             .then(() => "Success")
                             .catch(error => error.name);)";

  EXPECT_EQ(
      "Success",
      EvalJs(shell(),
             JsReplace(command,
                       server_.GetURL("a.test", "/cross-site/b.test/issue"))));

  EXPECT_EQ(true, EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
                                            IssuanceOriginFromHost("a.test"))));
  EXPECT_EQ(false,
            EvalJs(shell(), JsReplace("document.hasPrivateToken($1);",
                                      IssuanceOriginFromHost("b.test"))));

  // Expect one access for issue.
  EXPECT_EQ(1, access_count_);
}

// Issuance from a context with a secure-but-non-HTTP/S top frame origin
// should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       IssuanceRequiresSuitableTopFrameOrigin) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService();

  GURL file_url = GetTestUrl(/*dir=*/nullptr, "title1.html");
  ASSERT_TRUE(file_url.SchemeIsFile());

  ASSERT_TRUE(NavigateToURL(shell(), file_url));

  std::string command =
      R"(fetch($1, {privateToken: {version: 1,
                                 operation: 'token-request'}})
           .catch(error => error.name);)";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ("InvalidStateError",
            EvalJs(shell(), JsReplace(command, server_.GetURL("/issue"))));

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));
  EXPECT_EQ(
      false,
      EvalJs(shell(),
             JsReplace("document.hasPrivateToken($1);",
                       url::Origin::Create(server_.base_url()).Serialize())));

  // Expect one access for issue.
  EXPECT_EQ(1, access_count_);
}

// Redemption from a secure-but-non-HTTP(S) top frame origin should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       RedemptionRequiresSuitableTopFrameOrigin) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command =
      R"(fetch("/issue", {privateToken: {version: 1,
                                       operation: 'token-request'}})
                             .then(() => "Success")
                             .catch(error => error.name);)";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  EXPECT_EQ("Success", EvalJs(shell(), command));

  GURL file_url = GetTestUrl(/*dir=*/nullptr, "title1.html");

  ASSERT_TRUE(NavigateToURL(shell(), file_url));

  // Redemption from a page with a file:// top frame origin should fail.
  command = R"(fetch($1, {privateToken: {version: 1,
                                       operation: 'token-redemption'}})
                 .catch(error => error.name);)";
  EXPECT_EQ(
      "InvalidStateError",
      EvalJs(shell(), JsReplace(command, server_.GetURL("a.test", "/redeem"))));

  // Expect two accesses, one for issue and one for redemption.
  EXPECT_EQ(2, access_count_);
}

// hasPrivateToken from a context with a secure-but-non-HTTP/S top frame
// origin should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       HasTrustTokenRequiresSuitableTopFrameOrigin) {
  GURL file_url = GetTestUrl(/*dir=*/nullptr, "title1.html");
  ASSERT_TRUE(file_url.SchemeIsFile());
  ASSERT_TRUE(NavigateToURL(shell(), file_url));

  EXPECT_EQ("NotAllowedError",
            EvalJs(shell(),
                   R"(document.hasPrivateToken('https://issuer.example')
                              .catch(error => error.name);)"));

  EXPECT_EQ(0, access_count_);
}

// A hasPrivateToken call initiated from a secure context should succeed
// even if the initiating frame's origin is opaque (e.g. from a sandboxed
// iframe).
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       HasTrustTokenFromSecureSubframeWithOpaqueOrigin) {
  ASSERT_TRUE(NavigateToURL(
      shell(), server_.GetURL("a.test", "/page_with_sandboxed_iframe.html")));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ("Success",
            EvalJs(root->child_at(0)->current_frame_host(),
                   R"(document.hasPrivateToken('https://davids.website')
                              .then(()=>'Success');)"));

  EXPECT_EQ(0, access_count_);
}

// An operation initiated from a secure context should succeed even if the
// operation's associated request's initiator is opaque (e.g. from a sandboxed
// iframe with the right Permissions Policy).
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       OperationFromSecureSubframeWithOpaqueOrigin) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  ASSERT_TRUE(NavigateToURL(
      shell(), server_.GetURL("a.test", "/page_with_sandboxed_iframe.html")));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ("Success", EvalJs(root->child_at(0)->current_frame_host(),
                              JsReplace(R"(
                              fetch($1, {mode: 'no-cors',
                                         privateToken: {
                                             version: 1,
                                             operation: 'token-request'}
                                         }).then(()=>'Success');)",
                                        server_.GetURL("a.test", "/issue"))));

  // Expect one access for issue.
  EXPECT_EQ(1, access_count_);
}

// If a server issues with a key not present in the client's collection of key
// commitments, the issuance operation should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, IssuanceWithAbsentKeyFails) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  // Reset the handler, so that the client's valid keys disagree with the
  // server's keys. (This is theoretically flaky, but the chance of the client's
  // random keys colliding with the server's random keys is negligible.)
  request_handler_.UpdateOptions(TrustTokenRequestHandler::Options());

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command =
      R"(fetch($1, {privateToken: {version: 1,
                                                   operation: 'token-request'}})
                             .then(() => "Success")
                             .catch(error => error.name);)";
  EXPECT_EQ(
      "OperationError",
      EvalJs(shell(), JsReplace(command, server_.GetURL("a.test", "/issue"))));

  // Expect one access for issue.
  EXPECT_EQ(1, access_count_);
}

// This regression test for crbug.com/1111735 ensures it's possible to execute
// redemption from a nested same-origin frame that hasn't committed a
// navigation.
//
// How it works: The main frame embeds a same-origin iframe that does not
// commit a navigation (here, specifically because of an HTTP 204 return). From
// this iframe, we execute a Trust Tokens redemption operation via the iframe
// interface (in other words, the Trust Tokens operation executes during the
// process of navigating to a grandchild frame).  The grandchild frame's load
// will result in a renderer kill without the fix for the bug applied.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       SignFromFrameLackingACommittedNavigation) {
  GURL start_url = server_.GetURL(
      "a.test", "/page-executing-trust-token-signing-from-204-subframe.html");

  // Execute a signing operation from a child iframe that has not committed a
  // navigation (see the html source).
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  // For good measure, make sure the analogous signing operation works from
  // fetch, too, even though it wasn't broken by the same bug.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ("Success", EvalJs(root->child_at(0)->current_frame_host(),
                              JsReplace(R"(
                              fetch($1, {mode: 'no-cors',
                                         privateToken: {
                                             version: 1,
                                             operation: 'send-redemption-record',
                                             issuers: [
                                                 'https://issuer.example'
                                             ]}
                                         }).then(()=>'Success');)",
                                        server_.GetURL("a.test", "/issue"))));

  // Expect one access for sign.
  EXPECT_EQ(1, access_count_);
}

// Redemption should fail when there are no keys for the issuer.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RedemptionRequiresKeys) {
  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  EXPECT_EQ("InvalidStateError",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(() => 'Success')
        .catch(err => err.name); )",
                                      server_.GetURL("a.test", "/redeem"))));

  // Expect one access for redemption.
  EXPECT_EQ(1, access_count_);
}

// Redemption should fail when there are no tokens to redeem.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RedemptionRequiresTokens) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  EXPECT_EQ("OperationError",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(() => 'Success')
        .catch(err => err.name); )",
                                      server_.GetURL("a.test", "/redeem"))));

  // Expect one access for redemption.
  EXPECT_EQ(1, access_count_);
}

// When we have tokens for one issuer A, redemption against a different issuer B
// should still fail if we don't have any tokens for B.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       RedemptionWithoutTokensForDesiredIssuerFails) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/issue"))));

  EXPECT_EQ("OperationError",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(() => 'Success')
        .catch(err => err.name); )",
                                      server_.GetURL("b.test", "/redeem"))));

  // Expect two accesses, one for issuance and one for redemption.
  EXPECT_EQ(2, access_count_);
}

// When the server rejects redemption, the client-side redemption operation
// should fail.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       CorrectlyReportsServerErrorDuringRedemption) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  EXPECT_EQ("Success", EvalJs(shell(), R"(fetch('/issue',
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )"));

  // Send a redemption request to the issuance endpoint, which should error out
  // for the obvious reason that it isn't an issuance request:
  EXPECT_EQ("OperationError", EvalJs(shell(), R"(fetch('/issue',
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(() => 'Success')
        .catch(err => err.name); )"));

  // Expect two accesses, one for issuance and one for redemption.
  EXPECT_EQ(2, access_count_);
}

// After a successful issuance and redemption, a subsequent redemption against
// the same issuer should hit the redemption record cache.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       RedemptionHitsRedemptionRecordCache) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/issue"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/redeem"))));

  EXPECT_EQ("NoModificationAllowedError",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .catch(err => err.name); )",
                                      server_.GetURL("a.test", "/redeem"))));

  // Expect three accesses, one for issuance and two for redemption.
  EXPECT_EQ(3, access_count_);
}

// Redemption with `refresh-policy: 'refresh'` from an issuer context should
// succeed, overwriting the existing redemption record.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       RefreshPolicyRefreshWorksInIssuerContext) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/issue"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/redeem"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption',
                        refreshPolicy: 'refresh' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/redeem"))));

  // Expect three accesses, one for issuance and two for redemption.
  EXPECT_EQ(3, access_count_);
}

// Redemption with `refresh-policy: 'refresh'` from a non-issuer context should
// still work.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       RefreshPolicyRefreshRequiresIssuerContext) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"b.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  // Execute the operations against issuer https://b.test:<port> from a
  // different context; attempting to use refreshPolicy: 'refresh' should still
  // succeed.
  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("b.test", "/issue"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("b.test", "/redeem"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption',
                        refreshPolicy: 'refresh' } })
        .then(()=>'Success').catch(err => err.name); )",
                                      server_.GetURL("b.test", "/redeem"))));

  // Expect three accesses, one for issuance and two for redemption.
  EXPECT_EQ(3, access_count_);
}

// When a redemption request is made in cors mode, a cross-origin redirect from
// issuer A to issuer B should result in a new redemption request to issuer B,
// failing if there are no issuer B tokens.
//
// Note: For more on the interaction between Trust Tokens and redirects, see the
// "Handling redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
    TrustTokenBrowsertest,
    CorsModeCrossOriginRedirectRedemptionUsesNewOriginAsIssuer) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test", "b.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  // Obtain both https://a.test:<PORT> and https://b.test:<PORT> tokens, the
  // former for the initial redemption request to https://a.test:<PORT> and the
  // latter for the fresh post-redirect redemption request to
  // https://b.test:<PORT>.
  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/issue"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("b.test", "/issue"))));

  // On the redemption request, `mode: 'cors'` (the default) has the effect that
  // that redirecting a request will renew the request's Trust Tokens state.
  EXPECT_EQ("Success", EvalJs(shell(), R"(
      fetch('/cross-site/b.test/redeem',
        { privateToken: { mode: 'cors',
                        version: 1,
                        operation: 'token-redemption' } })
        .then(()=>'Success'); )"));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(
      fetch('/sign',
        { privateToken: { version: 1,
                        operation: 'send-redemption-record',
                        issuers: [$1],
        } }).then(()=>'Success');)",
                                      IssuanceOriginFromHost("b.test"))));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // When a signing operation fails, it isn't fatal, so the requests
  // should always get sent successfully.
  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(
      fetch('/sign',
        { privateToken: { version: 1,
                        operation: 'send-redemption-record',
                        issuers: [$1],
        } }).then(()=>'Success');)",
                                      IssuanceOriginFromHost("a.test"))));

  // There shouldn't have been an a.test redemption record attached to the
  // request.
  EXPECT_THAT(request_handler_.last_incoming_signed_request(),
              Optional(ReflectsSigningFailure()));

  // Expect six accesses, four for issuance and two for redemption.
  EXPECT_EQ(6, access_count_);
}

// When a redemption request is made in no-cors mode, a cross-origin redirect
// from issuer A to issuer B should result in recycling the original redemption
// request, obtaining an issuer A redemption record on success.
//
// Note: This isn't necessarily the behavior we'll end up wanting here; the test
// serves to document how redemption and redirects currently interact.  For more
// on the interaction between Trust Tokens and redirects, see the "Handling
// redirects" section in the design doc
// https://docs.google.com/document/d/1TNnya6B8pyomDK2F1R9CL3dY10OAmqWlnCxsWyOBDVQ/edit#heading=h.5erfr3uo012t
IN_PROC_BROWSER_TEST_F(
    TrustTokenBrowsertest,
    NoCorsModeCrossOriginRedirectRedemptionUsesOriginalOriginAsIssuer) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  EXPECT_EQ("Success", EvalJs(shell(), R"(
      fetch('/issue',
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )"));

  // `mode: 'no-cors'` on redemption has the effect that that redirecting a
  // request will maintain the request's Trust Tokens state.
  EXPECT_EQ("Success", EvalJs(shell(), R"(
      fetch('/cross-site/b.test/redeem',
        { mode: 'no-cors',
          privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(()=>'Success'); )"));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(
      fetch('/sign',
        { privateToken: { version: 1,
                        operation: 'send-redemption-record',
                        issuers: [$1]
        } })
        .then(()=>'Success'); )",
                                      IssuanceOriginFromHost("a.test"))));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // When a signing operation fails, it isn't fatal, so the requests
  // should always get sent successfully.
  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(
      fetch('/sign',
        { privateToken: { version: 1,
                        operation: 'send-redemption-record',
                        issuers: [$1]
        } })
            .then(()=>'Success'); )",
                                      IssuanceOriginFromHost("b.test"))));

  // There shouldn't have been a b.test redemption record attached to the
  // request.
  EXPECT_THAT(request_handler_.last_incoming_signed_request(),
              Optional(ReflectsSigningFailure()));

  // Expect four accesses, two for issuance and two for redemption.
  EXPECT_EQ(4, access_count_);
}

// When a redemption request is made in no-cors mode, a cross-origin redirect
// from issuer A to issuer B should result in recycling the original redemption
// request and, in particular, sending the same token.
//
// Note: This isn't necessarily the behavior we'll end up wanting here; the test
// serves to document how redemption and redirects currently interact.
IN_PROC_BROWSER_TEST_F(
    TrustTokenBrowsertest,
    NoCorsModeCrossOriginRedirectRedemptionRecyclesSameRedemptionRequest) {
  // Have issuance provide only a single token so that, if the redemption logic
  // searches for a new token after redirect, the redemption will fail.
  TrustTokenRequestHandler::Options options;
  options.batch_size = 1;
  request_handler_.UpdateOptions(std::move(options));

  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  EXPECT_EQ("Success", EvalJs(shell(), R"(
      fetch('/issue',
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )"));

  // The redemption should succeed after the redirect, yielding an a.test
  // redemption record (the redemption record correctly corresponding to a.test
  // is covered by a prior test case).
  EXPECT_EQ("Success", EvalJs(shell(), R"(
      fetch('/cross-site/b.test/redeem',
        { mode: 'no-cors',
          privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(()=>'Success'); )"));

  // Expect two accesses, one for issuance and one for redemption.
  EXPECT_EQ(2, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       SigningRequiresRedemptionRecordInStorage) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(
  (async () => {
    try {
      await fetch("/issue", {privateToken: {version: 1,
                                          operation: 'token-request'}});
      await fetch("/redeem", {privateToken: {version: 1,
                                           operation: 'token-redemption'}});
      await fetch("/sign", {privateToken: {
        version: 1,
        operation: 'send-redemption-record',
        issuers: [$1]}  // b.test, set below
      });
      return "Requests succeeded";
    } catch (err) {
      return "Requests failed unexpectedly";
    }
  })(); )";

  // We use EvalJs here, not ExecJs, because EvalJs waits for promises to
  // resolve.
  //
  // When a signing operation fails, it isn't fatal, so the requests
  // should always get sent successfully.
  EXPECT_EQ(
      "Requests succeeded",
      EvalJs(shell(), JsReplace(command, IssuanceOriginFromHost("b.test"))));

  EXPECT_THAT(request_handler_.last_incoming_signed_request(),
              Optional(ReflectsSigningFailure()));

  // Expect three access, one for issue, redeem, and sign.
  EXPECT_EQ(3, access_count_);
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, FetchEndToEndWithServiceWorker) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const char* const hostname = "a.test";
  ProvideRequestHandlerKeyCommitmentsToNetworkService({hostname});
  const std::string origin = IssuanceOriginFromHost(hostname);
  const GURL create_sw_url =
      server_.GetURL(hostname, "/service_worker/create_service_worker.html");
  EXPECT_TRUE(NavigateToURL(shell(), create_sw_url));
  // call register function defined in create_sw_url with the service worker js
  // file path
  EXPECT_EQ("DONE",
            EvalJs(shell(), "register('fetch_event_respond_with_fetch.js');"));
  // Following navigate to empty html page makes fetch requests go through
  // service worker. Requests do not go through service workers when commented
  // out.
  const GURL empty_page_url =
      server_.GetURL(hostname, "/service_worker/empty.html");
  EXPECT_TRUE(NavigateToURL(shell(), empty_page_url));
  const std::string trust_token_fetch_snippet = R"(
  (async () => {
    if (navigator.serviceWorker.controller === null) return "NotServiceWorker";
    await fetch("/issue", {privateToken: {version: 1,
                                        operation: 'token-request'}});
    await fetch("/redeem", {privateToken: {version: 1,
                                         operation: 'token-redemption'}});
    await fetch("/sign", {privateToken: {version: 1,
                                       operation: 'send-redemption-record',
                                  issuers: [$1]}});
    return "TTSuccess"; })(); )";

  EXPECT_EQ("TTSuccess",
            EvalJs(shell(), JsReplace(trust_token_fetch_snippet, origin)));

  EXPECT_THAT(
      request_handler_.last_incoming_signed_request(),
      Optional(AllOf(
          HasHeader(network::kTrustTokensRequestHeaderSecRedemptionRecord),
          HasHeader(network::kTrustTokensSecTrustTokenVersionHeader))));

  // Expect three accesses, one for issue and one for redeem and one for sign.
  EXPECT_EQ(3, access_count_);
}

// Test redemption limit. Make three refreshing redemption calls back to back
// and test whether the third one fails. This test does not mock time. It
// assumes time (in network process) elapsed between the first and the last
// redemption call is less than the hard coded limit (currently 48 hours).
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, RedemptionLimit) {
  // set request handler options batch size to more than 3
  TrustTokenRequestHandler::Options options;
  options.batch_size = 10;
  request_handler_.UpdateOptions(std::move(options));

  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});
  ASSERT_TRUE(NavigateToURL(shell(), server_.GetURL("a.test", "/title1.html")));

  // issue options.batch_size many tokens
  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-request' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/issue"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/redeem"))));

  EXPECT_EQ("Success",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption',
                        refreshPolicy: 'refresh' } })
        .then(()=>'Success'); )",
                                      server_.GetURL("a.test", "/redeem"))));
  // third redemption should fail
  EXPECT_EQ("Error",
            EvalJs(shell(), JsReplace(R"(fetch($1,
        { privateToken: { version: 1,
                        operation: 'token-redemption',
                        refreshPolicy: 'refresh' } })
        .then(()=>'Success')
        .catch(()=>'Error'); )",
                                      server_.GetURL("a.test", "/redeem"))));

  // Expect four accesses, one for issuance, one for redemption, and two for
  // sign.
  EXPECT_EQ(4, access_count_);
}

// Check whether depreciated fetch API where 'type' refers to operation
// type fails.
IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest, CheckDepreciatedTypeField) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(fetch(
    "/issue", {privateToken: {type: 'token-request'}})
    .then(()=>'Success')
    .catch(error => error.message); )";

  EXPECT_THAT(EvalJs(shell(), command).ExtractString(),
              HasSubstr("Failed to read the 'operation'\
 property from 'PrivateToken': Required member is undefined."));
}

IN_PROC_BROWSER_TEST_F(TrustTokenBrowsertest,
                       SendRedemptionRequestWithEmptyIssuers) {
  ProvideRequestHandlerKeyCommitmentsToNetworkService({"a.test"});

  GURL start_url = server_.GetURL("a.test", "/title1.html");
  ASSERT_TRUE(NavigateToURL(shell(), start_url));

  std::string command = R"(
  (async () => {
    await fetch("/issue", {privateToken: {version: 1,
                                        operation: 'token-request'}});
    await fetch("/redeem", {privateToken: {version: 1,
                                         operation: 'token-redemption'}});
    return "Success"; })(); )";
  ASSERT_EQ("Success", EvalJs(shell(), command));

  command = R"(
    fetch("/sign", {privateToken: {version: 1,
                                 operation: 'send-redemption-record',
                                 issuers: []}})
    .then(() => 'Success')
    .catch(error => error.message); )";

  // fetch should throw due to empty issuer field
  EXPECT_THAT(EvalJs(shell(), command).ExtractString(),
              HasSubstr("Failed to execute 'fetch' on 'Window':\
 privateToken: operation type 'send-redemption-record' requires that\
 the 'issuers' field be present and contain at least one secure,\
 HTTP(S) URL, but it was missing or empty."));
}

}  // namespace content