// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/web_package/web_bundle_parser.h"

#include <algorithm>
#include <optional>
#include <string_view>
#include <variant>

#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/rand_util.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "components/cbor/writer.h"
#include "components/web_package/mojom/web_bundle_parser.mojom.h"
#include "components/web_package/signed_web_bundles/ed25519_public_key.h"
#include "components/web_package/signed_web_bundles/integrity_block_attributes.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/web_package/test_support/signed_web_bundles/ed25519_key_pair.h"
#include "components/web_package/test_support/signed_web_bundles/web_bundle_signer.h"
#include "components/web_package/web_bundle_builder.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace web_package {

namespace {

constexpr char kPrimaryUrl[] = "https://test.example.com/";

std::string GetTestFileContents(const base::FilePath& path) {
  base::FilePath test_data_dir;
  base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &test_data_dir);
  test_data_dir = test_data_dir.Append(
      base::FilePath(FILE_PATH_LITERAL("components/test/data/web_package")));

  std::string contents;
  EXPECT_TRUE(base::ReadFileToString(test_data_dir.Append(path), &contents));
  return contents;
}

class TestDataSource : public mojom::BundleDataSource {
 public:
  explicit TestDataSource(const base::FilePath& path,
                          const bool is_random_access_context = false)
      : data_(GetTestFileContents(path)),
        is_random_access_context_(is_random_access_context) {}
  explicit TestDataSource(const std::vector<uint8_t>& data,
                          const bool is_random_access_context = false)
      : data_(reinterpret_cast<const char*>(data.data()), data.size()),
        is_random_access_context_(is_random_access_context) {}

  void Read(uint64_t offset, uint64_t length, ReadCallback callback) override {
    if (offset >= data_.size()) {
      std::move(callback).Run(std::nullopt);
      return;
    }
    const uint8_t* start =
        UNSAFE_TODO(reinterpret_cast<const uint8_t*>(data_.data()) + offset);
    uint64_t available_length = std::min(length, data_.size() - offset);
    std::move(callback).Run(
        std::vector<uint8_t>(start, UNSAFE_TODO(start + available_length)));
  }

  void Length(LengthCallback callback) override {
    EXPECT_TRUE(is_random_access_context_);
    std::move(callback).Run(data_.size());
  }

  void IsRandomAccessContext(IsRandomAccessContextCallback callback) override {
    std::move(callback).Run(is_random_access_context_);
  }

  std::string_view GetPayload(const mojom::BundleResponsePtr& response) {
    return std::string_view(data_).substr(response->payload_offset,
                                          response->payload_length);
  }

  void AddReceiver(mojo::PendingReceiver<mojom::BundleDataSource> receiver) {
    receivers_.Add(this, std::move(receiver));
  }

  void Close(CloseCallback callback) override {
    is_closed_ = true;
    std::move(callback).Run();
  }

  bool IsClosed() const { return is_closed_; }

 private:
  std::string data_;
  bool is_random_access_context_;
  mojo::ReceiverSet<mojom::BundleDataSource> receivers_;
  bool is_closed_ = false;
};

template <typename... T>
auto to_pair(std::tuple<T...>&& t)
    -> decltype(std::make_pair(std::get<0>(t), std::get<1>(t))) {
  return std::make_pair(std::move(std::get<0>(t)), std::move(std::get<1>(t)));
}

using ParseSignedBundleIntegrityBlockResult =
    base::expected<mojom::BundleIntegrityBlockPtr,
                   mojom::BundleIntegrityBlockParseErrorPtr>;

ParseSignedBundleIntegrityBlockResult ParseSignedBundleIntegrityBlock(
    TestDataSource* data_source,
    const GURL& base_url = GURL()) {
  mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());

  WebBundleParser parser_impl(std::move(source_remote), base_url);
  mojom::WebBundleParser& parser = parser_impl;

  base::test::TestFuture<mojom::BundleIntegrityBlockPtr,
                         mojom::BundleIntegrityBlockParseErrorPtr>
      integrity_block_future;
  parser.ParseIntegrityBlock(integrity_block_future.GetCallback());
  auto parse_result = to_pair(integrity_block_future.Take());
  EXPECT_TRUE((parse_result.first && !parse_result.second) ||
              (!parse_result.first && parse_result.second));

  if (parse_result.first) {
    return std::move(parse_result.first);
  } else {
    return base::unexpected(std::move(parse_result.second));
  }
}

using ParseUnsignedBundleResult =
    std::pair<mojom::BundleMetadataPtr, mojom::BundleMetadataParseErrorPtr>;

ParseUnsignedBundleResult ParseUnsignedBundle(
    TestDataSource* data_source,
    const GURL& base_url = GURL(),
    std::optional<uint64_t> offset = std::nullopt) {
  mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());

  WebBundleParser parser_impl(std::move(source_remote), base_url);
  mojom::WebBundleParser& parser = parser_impl;

  base::test::TestFuture<mojom::BundleMetadataPtr,
                         mojom::BundleMetadataParseErrorPtr>
      future;
  parser.ParseMetadata(offset, future.GetCallback());
  ParseUnsignedBundleResult result = to_pair(future.Take());
  EXPECT_TRUE((result.first && !result.second) ||
              (!result.first && result.second));
  return result;
}

void ExpectFormatError(ParseUnsignedBundleResult result) {
  ASSERT_TRUE(result.second);
  EXPECT_EQ(result.second->type, mojom::BundleParseErrorType::kFormatError);
}

// Finds the response for |url|.
mojom::BundleResponseLocationPtr FindResponse(
    const mojom::BundleMetadataPtr& metadata,
    const GURL& url) {
  const auto item = metadata->requests.find(url);
  if (item == metadata->requests.end()) {
    return nullptr;
  }

  return item->second.Clone();
}

mojom::BundleResponsePtr ParseResponse(
    TestDataSource* data_source,
    const mojom::BundleResponseLocationPtr& location,
    const GURL& base_url = GURL()) {
  mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());

  WebBundleParser parser_impl(std::move(source_remote), base_url);
  mojom::WebBundleParser& parser = parser_impl;

  base::test::TestFuture<mojom::BundleResponsePtr,
                         mojom::BundleResponseParseErrorPtr>
      future;
  parser.ParseResponse(location->offset, location->length,
                       future.GetCallback());
  return std::get<0>(future.Take());
}

std::vector<uint8_t> CreateSmallBundle() {
  web_package::WebBundleBuilder builder;
  builder.AddExchange(kPrimaryUrl,
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  return builder.CreateBundle();
}

struct SignedWebBundleAndKeys {
  std::vector<uint8_t> bundle;
  std::vector<test::KeyPair> key_pairs;
};

SignedWebBundleAndKeys SignBundle(
    const std::vector<uint8_t>& unsigned_bundle,
    std::optional<test::WebBundleSigner::IntegrityBlockAttributes>
        ib_attributes,
    test::WebBundleSigner::ErrorsForTesting errors_for_testing = {
        /*integrity_block_errors=*/{}, /*signatures_errors=*/{}},
    size_t num_signatures = 1) {
  std::vector<test::KeyPair> key_pairs;
  for (size_t i = 0; i < num_signatures; ++i) {
    key_pairs.push_back(test::Ed25519KeyPair::CreateRandom());
  }

  return {
      test::WebBundleSigner::SignBundle(unsigned_bundle, key_pairs,
                                        ib_attributes, errors_for_testing),
      key_pairs,
  };
}

SignedWebBundleAndKeys SignBundle(
    const std::vector<uint8_t>& unsigned_bundle,
    test::WebBundleSigner::ErrorsForTesting errors_for_testing = {
        /*integrity_block_errors=*/{}, /*signatures_errors=*/{}},
    size_t num_signatures = 1) {
  return SignBundle(unsigned_bundle, /*ib_attributes=*/{}, errors_for_testing,
                    num_signatures);
}

void CheckIfSignatureStackEntryIsValid(
    const mojom::BundleIntegrityBlockSignatureStackEntryPtr& entry,
    const Ed25519PublicKey& public_key) {
  ASSERT_TRUE(entry->signature_info->is_ed25519());
  EXPECT_EQ(entry->signature_info->get_ed25519()->public_key, public_key);

  // The attributes should contain the public key.
  EXPECT_NE(
      std::ranges::search(entry->attributes_cbor, public_key.bytes()).begin(),
      entry->attributes_cbor.end());
}

}  // namespace

using base::test::ErrorIs;
using testing::AllOf;
using testing::Eq;
using testing::Field;
using testing::HasSubstr;
using testing::Optional;
using testing::Pointee;
using testing::Property;

class WebBundleParserTest : public testing::Test {
 private:
  base::test::TaskEnvironment task_environment_;
};

TEST_F(WebBundleParserTest, WrongMagic) {
  WebBundleBuilder builder;
  std::vector<uint8_t> bundle = builder.CreateBundle();
  bundle[3] ^= 1;
  TestDataSource data_source(bundle);

  mojom::BundleMetadataParseErrorPtr error =
      ParseUnsignedBundle(&data_source).second;
  ASSERT_TRUE(error);
  EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
}

TEST_F(WebBundleParserTest, UnknownVersion) {
  WebBundleBuilder builder;
  std::vector<uint8_t> bundle = builder.CreateBundle();
  // Modify the version string from "b2\0\0" to "q2\0\0".
  ASSERT_EQ(bundle[11], 'b');
  bundle[11] = 'q';
  TestDataSource data_source(bundle);

  mojom::BundleMetadataParseErrorPtr error =
      ParseUnsignedBundle(&data_source).second;
  ASSERT_TRUE(error);
  EXPECT_EQ(error->type, mojom::BundleParseErrorType::kVersionError);
}

TEST_F(WebBundleParserTest, SectionLengthsTooLarge) {
  WebBundleBuilder builder;
  std::string too_long_section_name(8192, 'x');
  builder.AddSection(too_long_section_name, cbor::Value(0));
  TestDataSource data_source(builder.CreateBundle());

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, DuplicateSectionName) {
  WebBundleBuilder builder;
  builder.AddSection("foo", cbor::Value(0));
  builder.AddSection("foo", cbor::Value(0));
  TestDataSource data_source(builder.CreateBundle());

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, InvalidRequestURL) {
  WebBundleBuilder builder;
  builder.AddExchange("", {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, RequestURLIsNotUTF8) {
  WebBundleBuilder builder(BundleVersion::kB2,
                           /*allow_invalid_utf8_strings_for_testing*/ true);
  builder.AddExchange("https://test.example.com/\xcc",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

// TODO(crbug.com/40629011): Revisit this once
// https://github.com/WICG/webpackage/issues/468 is resolved.
TEST_F(WebBundleParserTest, RequestURLHasNonStandardScheme) {
  WebBundleBuilder builder;
  builder.AddExchange("foo://bar",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  ASSERT_TRUE(ParseUnsignedBundle(&data_source).first);
}

TEST_F(WebBundleParserTest, RequestURLHasIsolatedAppScheme) {
  WebBundleBuilder builder;
  builder.AddExchange("isolated-app://foo",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  ASSERT_TRUE(ParseUnsignedBundle(&data_source).first);
}

TEST_F(WebBundleParserTest, RequestURLHasCredentials) {
  WebBundleBuilder builder;
  builder.AddExchange("https://user:passwd@test.example.com/",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, RequestURLHasFragment) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/#fragment",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, RequestURLIsValidUuidInPackage) {
  const char uuid_in_package[] =
      "uuid-in-package:f81d4fae-7dec-11d0-a765-00a0c91e6bf6";
  WebBundleBuilder builder;
  builder.AddExchange(uuid_in_package,
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  ASSERT_EQ(metadata->requests.size(), 1u);
  auto location = FindResponse(metadata, GURL(uuid_in_package));
  ASSERT_TRUE(location);
}

TEST_F(WebBundleParserTest, NoStatusInResponseHeaders) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/",
                      {{"content-type", "text/plain"}},
                      "payload");  // ":status" is missing.
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  ASSERT_FALSE(ParseResponse(&data_source, location));
}

TEST_F(WebBundleParserTest, InvalidResponseStatus) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/",
                      {{":status", "0200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  ASSERT_FALSE(ParseResponse(&data_source, location));
}

TEST_F(WebBundleParserTest, ExtraPseudoInResponseHeaders) {
  WebBundleBuilder builder;
  builder.AddExchange(
      "https://test.example.com/",
      {{":status", "200"}, {":foo", ""}, {"content-type", "text/plain"}},
      "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  ASSERT_FALSE(ParseResponse(&data_source, location));
}

TEST_F(WebBundleParserTest, UpperCaseCharacterInHeaderName) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/",
                      {{":status", "200"}, {"Content-Type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  ASSERT_FALSE(ParseResponse(&data_source, location));
}

TEST_F(WebBundleParserTest, InvalidHeaderValue) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/",
                      {{":status", "200"}, {"content-type", "\n"}}, "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  ASSERT_FALSE(ParseResponse(&data_source, location));
}

TEST_F(WebBundleParserTest, NoContentTypeWithNonEmptyContent) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/", {{":status", "200"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  ASSERT_FALSE(ParseResponse(&data_source, location));
}

TEST_F(WebBundleParserTest, NoContentTypeWithEmptyContent) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/", {{":status", "301"}}, "");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  ASSERT_TRUE(ParseResponse(&data_source, location));
}

TEST_F(WebBundleParserTest, AllKnownSectionInCritical) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  cbor::Value::ArrayValue critical_section;
  critical_section.emplace_back("index");
  critical_section.emplace_back("critical");
  critical_section.emplace_back("responses");
  builder.AddSection("critical", cbor::Value(critical_section));
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
}

TEST_F(WebBundleParserTest, UnknownSectionInCritical) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  cbor::Value::ArrayValue critical_section;
  critical_section.emplace_back("unknown_section_name");
  builder.AddSection("critical", cbor::Value(critical_section));
  TestDataSource data_source(builder.CreateBundle());

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, ParseGoldenFile) {
  TestDataSource data_source(base::FilePath(FILE_PATH_LITERAL("hello_b2.wbn")));

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  ASSERT_EQ(metadata->requests.size(), 4u);
  EXPECT_EQ(metadata->primary_url, "https://test.example.org/");

  std::map<std::string, mojom::BundleResponsePtr> responses;
  for (const auto& item : metadata->requests) {
    auto location = FindResponse(metadata, item.first);
    ASSERT_TRUE(location);
    auto resp = ParseResponse(&data_source, location);
    ASSERT_TRUE(resp);
    responses[item.first.spec()] = std::move(resp);
  }

  ASSERT_TRUE(responses["https://test.example.org/"]);
  EXPECT_EQ(responses["https://test.example.org/"]->response_code, 200);
  EXPECT_EQ(
      responses["https://test.example.org/"]->response_headers["content-type"],
      "text/html; charset=utf-8");
  EXPECT_EQ(data_source.GetPayload(responses["https://test.example.org/"]),
            GetTestFileContents(
                base::FilePath(FILE_PATH_LITERAL("hello/index.html"))));

  EXPECT_TRUE(responses["https://test.example.org/index.html"]);
  EXPECT_TRUE(responses["https://test.example.org/manifest.webmanifest"]);
  EXPECT_TRUE(responses["https://test.example.org/script.js"]);
}

TEST_F(WebBundleParserTest, SingleEntry) {
  WebBundleBuilder builder;
  builder.AddPrimaryURL(kPrimaryUrl);
  builder.AddExchange("https://test.example.com/",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  ASSERT_EQ(metadata->version, mojom::BundleFormatVersion::kB2);
  ASSERT_EQ(metadata->requests.size(), 1u);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  auto response = ParseResponse(&data_source, location);
  ASSERT_TRUE(response);
  EXPECT_EQ(response->response_code, 200);
  EXPECT_EQ(response->response_headers.size(), 1u);
  EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  EXPECT_EQ(data_source.GetPayload(response), "payload");
  EXPECT_EQ(metadata->primary_url, kPrimaryUrl);
}

TEST_F(WebBundleParserTest, NoPrimaryUrlSingleEntry) {
  WebBundleBuilder builder;
  builder.AddExchange("https://test.example.com/",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);
  ASSERT_EQ(metadata->requests.size(), 1u);
  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  auto response = ParseResponse(&data_source, location);
  ASSERT_TRUE(response);
  EXPECT_EQ(response->response_code, 200);
  EXPECT_EQ(response->response_headers.size(), 1u);
  EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  EXPECT_EQ(data_source.GetPayload(response), "payload");
  EXPECT_FALSE(metadata->primary_url.has_value());
}

TEST_F(WebBundleParserTest, RelativeURL) {
  WebBundleBuilder builder;
  builder.AddPrimaryURL("path/to/primary_url");
  builder.AddExchange("path/to/file.txt",
                      {{":status", "200"}, {"content-type", "text/plain"}},
                      "payload");
  TestDataSource data_source(builder.CreateBundle());

  const GURL base_url("https://test.example.com/dir/test.wbn");
  mojom::BundleMetadataPtr metadata =
      ParseUnsignedBundle(&data_source, base_url).first;
  ASSERT_TRUE(metadata);
  EXPECT_EQ(metadata->primary_url,
            "https://test.example.com/dir/path/to/primary_url");
  ASSERT_TRUE(metadata);
  ASSERT_EQ(metadata->requests.size(), 1u);
  auto location = FindResponse(
      metadata, GURL("https://test.example.com/dir/path/to/file.txt"));
  ASSERT_TRUE(location);
  auto response = ParseResponse(&data_source, location, base_url);
  ASSERT_TRUE(response);
  EXPECT_EQ(response->response_code, 200);
  EXPECT_EQ(response->response_headers.size(), 1u);
  EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  EXPECT_EQ(data_source.GetPayload(response), "payload");
}

TEST_F(WebBundleParserTest, RandomAccessContextWithAutomaticOffset) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);

  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  auto response = ParseResponse(&data_source, location);
  ASSERT_TRUE(response);
  EXPECT_EQ(response->response_code, 200);
  EXPECT_EQ(response->response_headers.size(), 1u);
  EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  EXPECT_EQ(data_source.GetPayload(response), "payload");
}

TEST_F(WebBundleParserTest, RandomAccessContextWithFixedCorrectOffset) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  mojom::BundleMetadataPtr metadata =
      ParseUnsignedBundle(&data_source, GURL(), 0).first;
  ASSERT_TRUE(metadata);

  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  auto response = ParseResponse(&data_source, location);
  ASSERT_TRUE(response);
  EXPECT_EQ(response->response_code, 200);
  EXPECT_EQ(response->response_headers.size(), 1u);
  EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  EXPECT_EQ(data_source.GetPayload(response), "payload");
}

TEST_F(WebBundleParserTest, RandomAccessContextWithFixedIncorrectOffset) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  mojom::BundleMetadataParseErrorPtr error =
      ParseUnsignedBundle(&data_source, GURL(), 1).second;
  ASSERT_TRUE(error);
}

TEST_F(WebBundleParserTest,
       RandomAccessContextPrependedDataWithAutomaticOffset) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  bundle.insert(bundle.begin(),
                {'o', 't', 'h', 'e', 'r', ' ', 'd', 'a', 't', 'a'});
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  ASSERT_TRUE(metadata);

  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  auto response = ParseResponse(&data_source, location);
  ASSERT_TRUE(response);
  EXPECT_EQ(response->response_code, 200);
  EXPECT_EQ(response->response_headers.size(), 1u);
  EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  EXPECT_EQ(data_source.GetPayload(response), "payload");
}

TEST_F(WebBundleParserTest,
       RandomAccessContextPrependedDataWithFixedCorrectOffset) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  bundle.insert(bundle.begin(),
                {'o', 't', 'h', 'e', 'r', ' ', 'd', 'a', 't', 'a'});
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  mojom::BundleMetadataPtr metadata =
      ParseUnsignedBundle(&data_source, GURL(), 10).first;
  ASSERT_TRUE(metadata);

  auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  ASSERT_TRUE(location);
  auto response = ParseResponse(&data_source, location);
  ASSERT_TRUE(response);
  EXPECT_EQ(response->response_code, 200);
  EXPECT_EQ(response->response_headers.size(), 1u);
  EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  EXPECT_EQ(data_source.GetPayload(response), "payload");
}

TEST_F(WebBundleParserTest,
       RandomAccessContextPrependedDataWithFixedIncorrectOffset) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  bundle.insert(bundle.begin(),
                {'o', 't', 'h', 'e', 'r', ' ', 'd', 'a', 't', 'a'});
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  mojom::BundleMetadataParseErrorPtr error =
      ParseUnsignedBundle(&data_source, GURL(), 3).second;
  ASSERT_TRUE(error);
}

TEST_F(WebBundleParserTest, RandomAccessContextLengthSmallerThanWebBundle) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  std::vector<uint8_t> invalid_length = {0, 0, 0, 0, 0, 0, 0, 10};
  std::ranges::copy(invalid_length, bundle.end() - 8);
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, RandomAccessContextFileSmallerThanLengthField) {
  std::vector<uint8_t> bundle = {1, 2, 3, 4};
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

TEST_F(WebBundleParserTest, RandomAccessContextLengthBiggerThanFile) {
  std::vector<uint8_t> bundle = CreateSmallBundle();
  std::vector<uint8_t> invalid_length = {0xff, 0, 0, 0, 0, 0, 0, 0};
  std::ranges::copy(invalid_length, bundle.end() - 8);
  TestDataSource data_source(bundle, /*is_random_access_context=*/true);

  ExpectFormatError(ParseUnsignedBundle(&data_source));
}

// TODO(crbug.com/40630324): Add a test case that loads a wbn file with
// variants, once gen-bundle supports variants.

// This test verifies that even if a bundle is signed, it is still readable as
// an unsigned bundle in random-access contexts, since the `length` field of the
// web bundle can be used to find the start of the unsigned bundle.
TEST_F(WebBundleParserTest, SignedBundleMetadataOnlyInRandomAccessContexts) {
  auto bundle_and_keys = SignBundle(CreateSmallBundle());
  TestDataSource data_source(bundle_and_keys.bundle, true);

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  EXPECT_TRUE(metadata);
}

// This test verifies that when a bundle is signed, it can not be read as an
// unsigned bundle in non-random-access contexts, since the `length` field of
// the web bundle can't be used then.
TEST_F(WebBundleParserTest, SignedBundleMetadataOnlyInNonRandomAccessContexts) {
  auto bundle_and_keys = SignBundle(CreateSmallBundle());
  TestDataSource data_source(bundle_and_keys.bundle, false);

  mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  EXPECT_FALSE(metadata);
}

TEST_F(WebBundleParserTest, SignedBundleIntegrityBlockIsParsedCorrectly) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys = SignBundle(unsigned_bundle);
  TestDataSource data_source(bundle_and_keys.bundle);

  ASSERT_OK_AND_ASSIGN(auto integrity_block,
                       ParseSignedBundleIntegrityBlock(&data_source));

  // The size of the integrity block should be exactly equal to the size
  // difference between a signed and an unsigned bundle.
  EXPECT_EQ(integrity_block->size,
            bundle_and_keys.bundle.size() - unsigned_bundle.size());

  // There should be exactly one signature stack entry, corresponding to the
  // public key that was used to sign the web bundle.
  EXPECT_EQ(integrity_block->signature_stack.size(), 1ul);
  auto& entry = integrity_block->signature_stack[0];
  EXPECT_NO_FATAL_FAILURE(CheckIfSignatureStackEntryIsValid(
      entry,
      std::get<test::Ed25519KeyPair>(bundle_and_keys.key_pairs[0]).public_key));
}

TEST_F(WebBundleParserTest,
       SignedBundleSignatureStackWithMultipleEntries_AllValid) {
  unsigned long num_signatures = base::RandIntInclusive(2, 15);

  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle, /*errors_for_testing=*/
                 {/*integrity_block_errors=*/{}, /*signatures_errors=*/{}},
                 num_signatures);
  TestDataSource data_source(bundle_and_keys.bundle);

  ASSERT_OK_AND_ASSIGN(auto integrity_block,
                       ParseSignedBundleIntegrityBlock(&data_source));

  // The size of the integrity block should be exactly equal to the size
  // difference between a signed and an unsigned bundle.
  EXPECT_EQ(integrity_block->size,
            bundle_and_keys.bundle.size() - unsigned_bundle.size());

  // The signature stack should contain the expected number of signatures, and
  // each entry should correspond to the public key that was used to sign the
  // web bundle.
  EXPECT_EQ(integrity_block->signature_stack.size(), num_signatures);

  for (unsigned long i = 0; i < num_signatures; ++i) {
    EXPECT_NO_FATAL_FAILURE(CheckIfSignatureStackEntryIsValid(
        integrity_block->signature_stack[i],
        std::get<test::Ed25519KeyPair>(bundle_and_keys.key_pairs[i])
            .public_key));
  }
}

TEST_F(WebBundleParserTest,
       SignedBundleSignatureStackWithMultipleEntries_SomeInvalid) {
  std::vector<test::WebBundleSigner::IntegritySignatureErrorsForTesting>
      signatures_errors = {
          {},
          {},
          {test::WebBundleSigner::IntegritySignatureErrorForTesting::
               kWrongSignatureStackEntryAttributeNameLength},
          {test::WebBundleSigner::IntegritySignatureErrorForTesting::
               kWrongSignatureStackEntryAttributeNameLength},
          {test::WebBundleSigner::IntegritySignatureErrorForTesting::
               kWrongSignatureStackEntryAttributeNameLength},
          {},
          {}};
  auto total_signatures = signatures_errors.size();

  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys = SignBundle(
      unsigned_bundle, {/*integrity_block_errors=*/{}, signatures_errors},
      total_signatures);
  TestDataSource data_source(bundle_and_keys.bundle);

  ASSERT_OK_AND_ASSIGN(auto integrity_block,
                       ParseSignedBundleIntegrityBlock(&data_source));

  // The size of the integrity block should be exactly equal to the size
  // difference between a signed and an unsigned bundle.
  EXPECT_EQ(integrity_block->size,
            bundle_and_keys.bundle.size() - unsigned_bundle.size());

  // The signature stack should contain all the signatures, with the invalid
  // signatures of type unknown and each valid entry should correspond to the
  // public key that was used to sign the web bundle.
  EXPECT_EQ(integrity_block->signature_stack.size(), total_signatures);

  for (size_t index = 0; index < total_signatures; ++index) {
    if (signatures_errors[index].empty()) {
      auto* key_pair =
          std::get_if<test::Ed25519KeyPair>(&bundle_and_keys.key_pairs[index]);
      EXPECT_NO_FATAL_FAILURE(CheckIfSignatureStackEntryIsValid(
          integrity_block->signature_stack[index], key_pair->public_key));
    } else {
      EXPECT_TRUE(integrity_block->signature_stack[index]
                      ->signature_info->is_unknown());
    }
  }
}

TEST_F(WebBundleParserTest,
       SignedBundleSignatureStackWithMultipleEntries_FirstInvalid) {
  size_t total_signatures = 3;
  std::vector<test::WebBundleSigner::IntegritySignatureErrorsForTesting>
      signatures_errors(total_signatures);
  signatures_errors[0] = {
      test::WebBundleSigner::IntegritySignatureErrorForTesting::
          kWrongSignatureStackEntryAttributeNameLength};

  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys = SignBundle(
      unsigned_bundle, {/*integrity_block_errors=*/{}, signatures_errors},
      total_signatures);
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(AllOf(
                  Field(&mojom::BundleIntegrityBlockParseError::type,
                        Eq(mojom::BundleParseErrorType::kFormatError)),
                  Field(&mojom::BundleIntegrityBlockParseError::message,
                        Eq("Unknown cipher type of the first signature."))))));
}

TEST_F(WebBundleParserTest, SignedBundleWrongMagic) {
  WebBundleBuilder builder;
  std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  auto bundle_and_keys = SignBundle(unsigned_bundle);
  bundle_and_keys.bundle[3] ^= 1;
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("Unexpected magic bytes."))))));
}

TEST_F(WebBundleParserTest, SignedBundleUnknownVersion) {
  WebBundleBuilder builder;
  std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  auto bundle_and_keys = SignBundle(unsigned_bundle);
  // Modify the version string from "1b\0\0" to "1q\0\0".
  ASSERT_EQ(bundle_and_keys.bundle[12], 'b');
  bundle_and_keys.bundle[12] = 'q';
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(
      ParseSignedBundleIntegrityBlock(&data_source),
      ErrorIs(Pointee(Field(&mojom::BundleIntegrityBlockParseError::type,
                            Eq(mojom::BundleParseErrorType::kVersionError)))));
}

TEST_F(WebBundleParserTest, SignedBundleEmptySignatureStack) {
  std::vector<uint8_t> signed_bundle = test::WebBundleSigner::SignBundle(
      WebBundleBuilder().CreateBundle(), /*key_pairs=*/{}, /*ib_attributes=*/
      {{.web_bundle_id =
            "amoiebz32b7o24tilu257xne2yf3nkblkploanxzm7ebeglseqpfeaacai"}},
      {/*integrity_block_errors=*/{
           test::WebBundleSigner::IntegrityBlockErrorForTesting::
               kEmptySignatureList},
       /*signatures_errors=*/{}});
  TestDataSource data_source(signed_bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("The signature stack must contain at least "
                                 "one signature."))))));
}

TEST_F(WebBundleParserTest, SignedBundleNoBundleId) {
  WebBundleBuilder builder;
  std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{
                      test::WebBundleSigner::IntegrityBlockErrorForTesting::
                          kNoSignedWebBundleId},
                  /*signatures_errors=*/{}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("`webBundleId` integrity block attribute is "
                                 "missing or malformed."))))));
}

TEST_F(WebBundleParserTest, SignedBundleNoAttributes) {
  WebBundleBuilder builder;
  std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  auto bundle_and_keys = SignBundle(
      unsigned_bundle,
      {/*integrity_block_errors=*/{
           test::WebBundleSigner::IntegrityBlockErrorForTesting::kNoAttributes},
       /*signatures_errors=*/{}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("Invalid integrity block array length: "
                                 "expected 4, got 3."))))));
}

TEST_F(WebBundleParserTest, SignedBundleWrongSignatureLength) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kInvalidSignatureLength}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("The signature has the wrong length. Expected "
                                 "64, but got 65 bytes."))))));
}

TEST_F(WebBundleParserTest, SignedBundleWrongSignatureStackEntryLength) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kAdditionalSignatureStackEntryElement}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("Each signature stack entry must contain "
                                 "exactly two elements."))))));
}

TEST_F(WebBundleParserTest, SignedBundleWithMultipleAttributes) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kAdditionalSignatureStackEntryAttributes}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  ASSERT_OK_AND_ASSIGN(auto integrity_block,
                       ParseSignedBundleIntegrityBlock(&data_source));

  // The size of the integrity block should be exactly equal to the size
  // difference between a signed and an unsigned bundle.
  EXPECT_EQ(integrity_block->size,
            bundle_and_keys.bundle.size() - unsigned_bundle.size());

  // There should be exactly one signature stack entry, corresponding to the
  // public key that was used to sign the web bundle.
  EXPECT_EQ(integrity_block->signature_stack.size(), 1ul);
  auto& entry = integrity_block->signature_stack[0];
  EXPECT_NO_FATAL_FAILURE(CheckIfSignatureStackEntryIsValid(
      entry,
      std::get<test::Ed25519KeyPair>(bundle_and_keys.key_pairs[0]).public_key));
}

TEST_F(WebBundleParserTest, SignedBundleV2) {
  static constexpr std::string_view kWebBundleId =
      "aerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys = SignBundle(
      unsigned_bundle, {{.web_bundle_id = std::string(kWebBundleId)}},
      {/*integrity_block_errors=*/{},
       {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
             kAdditionalSignatureStackEntryAttributes}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  ASSERT_OK_AND_ASSIGN(auto integrity_block,
                       ParseSignedBundleIntegrityBlock(&data_source));

  // The size of the integrity block should be exactly equal to the size
  // difference between a signed and an unsigned bundle.
  EXPECT_EQ(integrity_block->size,
            bundle_and_keys.bundle.size() - unsigned_bundle.size());

  EXPECT_THAT(
      integrity_block->attributes,
      Property(&IntegrityBlockAttributes::web_bundle_id, Eq(kWebBundleId)));

  // There should be exactly one signature stack entry, corresponding to the
  // public key that was used to sign the web bundle.
  EXPECT_EQ(integrity_block->signature_stack.size(), 1ul);
  auto& entry = integrity_block->signature_stack[0];
  EXPECT_NO_FATAL_FAILURE(CheckIfSignatureStackEntryIsValid(
      entry,
      std::get<test::Ed25519KeyPair>(bundle_and_keys.key_pairs[0]).public_key));
}

TEST_F(WebBundleParserTest, SignedBundleWithMultiplePublicKeyAttributes) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kMultipleValidPublicKeyAttributes}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("Multiple key types for one signature."))))));
}

TEST_F(WebBundleParserTest, SignedBundleUnsupportedSignatureAttributeMap) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kSignatureStackEntryUnsupportedMapAttribute}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("Invalid ECDSA key."))))));
}

TEST_F(WebBundleParserTest, SignedBundleUnsupportedSignatureAttributeArray) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kSignatureStackEntryUnsupportedArrayAttribute}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("Invalid ED25519 key."))))));
}

TEST_F(WebBundleParserTest, SignedBundleNoPublicKeyAttribute) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kNoPublicKeySignatureStackEntryAttribute}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(AllOf(
                  Field(&mojom::BundleIntegrityBlockParseError::type,
                        Eq(mojom::BundleParseErrorType::kFormatError)),
                  Field(&mojom::BundleIntegrityBlockParseError::message,
                        Eq("Unknown cipher type of the first signature."))))));
}

TEST_F(WebBundleParserTest, SignedBundleWrongPublicKeyAttributeName) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kWrongSignatureStackEntryAttributeName}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(AllOf(
                  Field(&mojom::BundleIntegrityBlockParseError::type,
                        Eq(mojom::BundleParseErrorType::kFormatError)),
                  Field(&mojom::BundleIntegrityBlockParseError::message,
                        Eq("Unknown cipher type of the first signature."))))));
}

TEST_F(WebBundleParserTest, SignedBundleWrongPublicKeyAttributeLength) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kWrongSignatureStackEntryAttributeNameLength}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(AllOf(
                  Field(&mojom::BundleIntegrityBlockParseError::type,
                        Eq(mojom::BundleParseErrorType::kFormatError)),
                  Field(&mojom::BundleIntegrityBlockParseError::message,
                        Eq("Unknown cipher type of the first signature."))))));
}

TEST_F(WebBundleParserTest, SignedBundleWrongPublicKeyLength) {
  auto unsigned_bundle = CreateSmallBundle();
  auto bundle_and_keys =
      SignBundle(unsigned_bundle,
                 {/*integrity_block_errors=*/{},
                  {{test::WebBundleSigner::IntegritySignatureErrorForTesting::
                        kInvalidPublicKeyLength}}});
  TestDataSource data_source(bundle_and_keys.bundle);

  EXPECT_THAT(ParseSignedBundleIntegrityBlock(&data_source),
              ErrorIs(Pointee(
                  AllOf(Field(&mojom::BundleIntegrityBlockParseError::type,
                              Eq(mojom::BundleParseErrorType::kFormatError)),
                        Field(&mojom::BundleIntegrityBlockParseError::message,
                              Eq("The Ed25519 public key does not have the "
                                 "correct length. Expected "
                                 "32 bytes, but received 33 bytes."))))));
}

TEST_F(WebBundleParserTest, DisconnectWhileParsingMetadata) {
  base::test::TestFuture<mojom::BundleMetadataPtr,
                         mojom::BundleMetadataParseErrorPtr>
      future;
  {
    WebBundleBuilder builder;
    builder.AddPrimaryURL(kPrimaryUrl);
    builder.AddExchange("https://test.example.com/",
                        {{":status", "200"}, {"content-type", "text/plain"}},
                        "payload");
    TestDataSource data_source(builder.CreateBundle());

    mojo::PendingRemote<mojom::BundleDataSource> source_remote;
    data_source.AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());

    WebBundleParser parser_impl(std::move(source_remote), GURL());
    mojom::WebBundleParser& parser = parser_impl;

    parser.ParseMetadata(/*offset=*/std::nullopt, future.GetCallback());
    // |data_source| and |parser_impl| are deleted here.
  }

  auto error = std::get<1>(future.Take());
  ASSERT_TRUE(error);
  EXPECT_EQ(error->type, mojom::BundleParseErrorType::kParserInternalError);
  EXPECT_EQ(error->message, "Data source disconnected.");
}

TEST_F(WebBundleParserTest, DisconnectWhileParsingResponse) {
  base::test::TestFuture<mojom::BundleResponsePtr,
                         mojom::BundleResponseParseErrorPtr>
      future;
  {
    WebBundleBuilder builder;
    builder.AddPrimaryURL(kPrimaryUrl);
    builder.AddExchange("https://test.example.com/",
                        {{":status", "200"}, {"content-type", "text/plain"}},
                        "payload");
    TestDataSource data_source(builder.CreateBundle());

    mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
    ASSERT_TRUE(metadata);
    auto location = FindResponse(metadata, GURL("https://test.example.com/"));
    ASSERT_TRUE(location);

    mojo::PendingRemote<mojom::BundleDataSource> source_remote;
    data_source.AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());

    WebBundleParser parser_impl(std::move(source_remote), GURL());
    mojom::WebBundleParser& parser = parser_impl;

    parser.ParseResponse(location->offset, location->length,
                         future.GetCallback());
    // |data_source| and |parser_impl| are deleted here.
  }

  auto error = std::get<1>(future.Take());
  ASSERT_TRUE(error);
  EXPECT_EQ(error->type, mojom::BundleParseErrorType::kParserInternalError);
  EXPECT_EQ(error->message, "Data source disconnected.");
}

// This data source implementation never run result callback
// making the calls to it permanently pending.
class BlockingDataSource : public mojom::BundleDataSource {
 public:
  void Read(uint64_t offset, uint64_t length, ReadCallback callback) override {}
  void Length(LengthCallback callback) override {}
  void IsRandomAccessContext(IsRandomAccessContextCallback callback) override {}
  void Close(CloseCallback callback) override {}
};

TEST_F(WebBundleParserTest, DestructorWhileParsing) {
  base::test::TestFuture<mojom::BundleResponsePtr,
                         mojom::BundleResponseParseErrorPtr>
      response_future;
  base::test::TestFuture<mojom::BundleMetadataPtr,
                         mojom::BundleMetadataParseErrorPtr>
      metadata_future;
  base::test::TestFuture<mojom::BundleIntegrityBlockPtr,
                         mojom::BundleIntegrityBlockParseErrorPtr>
      integrity_block_future;

  mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  mojo::MakeSelfOwnedReceiver(std::make_unique<BlockingDataSource>(),
                              source_remote.InitWithNewPipeAndPassReceiver());
  {
    WebBundleParser parser_impl(std::move(source_remote), GURL());
    mojom::WebBundleParser& parser = parser_impl;

    parser.ParseResponse(/*response_offset=*/100, /*response_length=*/1234,
                         response_future.GetCallback());
    parser.ParseMetadata(/*offset=*/std::nullopt,
                         metadata_future.GetCallback());
    parser.ParseIntegrityBlock(integrity_block_future.GetCallback());
    //|parser_impl| are deleted here.
  }

  {
    auto error_response = std::get<1>(response_future.Take());
    ASSERT_TRUE(error_response);
    EXPECT_EQ(error_response->type,
              mojom::BundleParseErrorType::kParserInternalError);
    EXPECT_EQ(error_response->message, "Data source disconnected.");
  }

  {
    auto error_metadata = std::get<1>(metadata_future.Take());
    ASSERT_TRUE(error_metadata);
    EXPECT_EQ(error_metadata->type,
              mojom::BundleParseErrorType::kParserInternalError);
    EXPECT_EQ(error_metadata->message, "Data source disconnected.");
  }

  {
    auto error_integrity_block = std::get<1>(integrity_block_future.Take());
    ASSERT_TRUE(error_integrity_block);
    EXPECT_EQ(error_integrity_block->type,
              mojom::BundleParseErrorType::kParserInternalError);
    EXPECT_EQ(error_integrity_block->message, "Data source disconnected.");
  }
}

TEST_F(WebBundleParserTest, Close) {
  auto unsigned_bundle = CreateSmallBundle();
  TestDataSource data_source(unsigned_bundle);
  EXPECT_FALSE(data_source.IsClosed());

  mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  data_source.AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());

  WebBundleParser parser_impl(std::move(source_remote), GURL());
  mojom::WebBundleParser& parser = parser_impl;

  base::test::TestFuture<void> future;
  parser.Close(future.GetCallback());
  future.Get();
  EXPECT_TRUE(data_source.IsClosed());
}

}  // namespace web_package
