123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- // Copyright 2020 The Chromium Authors. All rights reserved.
- // 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_url_loader_factory.h"
- #include "base/callback_helpers.h"
- #include "base/test/metrics/histogram_tester.h"
- #include "base/test/task_environment.h"
- #include "components/web_package/web_bundle_builder.h"
- #include "components/web_package/web_bundle_memory_quota_consumer.h"
- #include "mojo/public/cpp/bindings/remote.h"
- #include "mojo/public/cpp/system/data_pipe_utils.h"
- #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
- #include "services/network/public/cpp/resource_request.h"
- #include "services/network/public/mojom/url_loader.mojom.h"
- #include "services/network/public/mojom/url_loader_factory.mojom.h"
- #include "services/network/test/mock_devtools_observer.h"
- #include "services/network/test/test_url_loader_client.h"
- #include "testing/gmock/include/gmock/gmock.h"
- #include "testing/gtest/include/gtest/gtest.h"
- namespace web_package {
- namespace {
- const char kInitiatorUrl[] = "https://example.com/";
- const char kBundleUrl[] = "https://example.com/bundle.wbn";
- const char kBundleRequestId[] = "bundle-devtools-request-id";
- const char kResourceUrl[] = "https://example.com/";
- const char kResourceUrl2[] = "https://example.com/another";
- const char kResourceUrl3[] = "https://example.com/yetanother";
- const char kInvalidResourceUrl[] = "ftp://foo";
- const char kResourceRequestId[] = "resource-1-devtools-request-id";
- const char kResourceRequestId2[] = "resource-2-devtools-request-id";
- const char kResourceRequestId3[] = "resource-3-devtools-request-id";
- // Cross origin resources
- const char kCrossOriginJsonUrl[] = "https://other.com/resource.json";
- const char kCrossOriginJsUrl[] = "https://other.com/resource.js";
- using ::testing::_;
- using ::testing::ElementsAre;
- using ::testing::Eq;
- using ::testing::Optional;
- using ::testing::Pointee;
- std::vector<uint8_t> CreateSmallBundle() {
- web_package::WebBundleBuilder builder;
- builder.AddExchange(kResourceUrl,
- {{":status", "200"}, {"content-type", "text/plain"}},
- "body");
- return builder.CreateBundle();
- }
- std::vector<uint8_t> CreateLargeBundle() {
- web_package::WebBundleBuilder builder;
- builder.AddExchange(kResourceUrl,
- {{":status", "200"}, {"content-type", "text/plain"}},
- "body");
- builder.AddExchange(kResourceUrl2,
- {{":status", "200"}, {"content-type", "text/plain"}},
- std::string(10000, 'a'));
- builder.AddExchange(kResourceUrl3,
- {{":status", "200"}, {"content-type", "text/plain"}},
- "body");
- return builder.CreateBundle();
- }
- std::vector<uint8_t> CreateCrossOriginBundle() {
- web_package::WebBundleBuilder builder;
- builder.AddExchange(
- kCrossOriginJsonUrl,
- {{":status", "200"}, {"content-type", "application/json"}},
- "{ secret: 1 }");
- builder.AddExchange(kCrossOriginJsUrl,
- {{":status", "200"}, {"content-type", "application/js"}},
- "const not_secret = 1;");
- return builder.CreateBundle();
- }
- class TestWebBundleHandle : public network::mojom::WebBundleHandle {
- public:
- explicit TestWebBundleHandle(
- mojo::PendingReceiver<network::mojom::WebBundleHandle> receiver)
- : receiver_(this, std::move(receiver)) {}
- const absl::optional<
- std::pair<network::mojom::WebBundleErrorType, std::string>>&
- last_bundle_error() const {
- return last_bundle_error_;
- }
- void RunUntilBundleError() {
- if (last_bundle_error_.has_value())
- return;
- base::RunLoop run_loop;
- quit_closure_for_bundle_error_ = run_loop.QuitClosure();
- run_loop.Run();
- }
- // network::mojom::WebBundleHandle
- void Clone(mojo::PendingReceiver<network::mojom::WebBundleHandle> receiver)
- override {
- NOTREACHED();
- }
- void OnWebBundleError(network::mojom::WebBundleErrorType type,
- const std::string& message) override {
- last_bundle_error_ = std::make_pair(type, message);
- if (quit_closure_for_bundle_error_)
- std::move(quit_closure_for_bundle_error_).Run();
- }
- void OnWebBundleLoadFinished(bool success) override {}
- private:
- mojo::Receiver<network::mojom::WebBundleHandle> receiver_;
- absl::optional<std::pair<network::mojom::WebBundleErrorType, std::string>>
- last_bundle_error_;
- base::OnceClosure quit_closure_for_bundle_error_;
- };
- class MockMemoryQuotaConsumer : public WebBundleMemoryQuotaConsumer {
- public:
- MockMemoryQuotaConsumer() = default;
- ~MockMemoryQuotaConsumer() override = default;
- bool AllocateMemory(uint64_t num_bytes) override { return true; }
- };
- class BadMessageTestHelper {
- public:
- BadMessageTestHelper()
- : dummy_message_(0, 0, 0, 0, nullptr), context_(&dummy_message_) {
- mojo::SetDefaultProcessErrorHandler(base::BindRepeating(
- &BadMessageTestHelper::OnBadMessage, base::Unretained(this)));
- }
- BadMessageTestHelper(const BadMessageTestHelper&) = delete;
- BadMessageTestHelper& operator=(const BadMessageTestHelper&) = delete;
- ~BadMessageTestHelper() {
- mojo::SetDefaultProcessErrorHandler(base::NullCallback());
- }
- const std::vector<std::string>& bad_message_reports() const {
- return bad_message_reports_;
- }
- private:
- void OnBadMessage(const std::string& reason) {
- bad_message_reports_.push_back(reason);
- }
- std::vector<std::string> bad_message_reports_;
- mojo::Message dummy_message_;
- mojo::internal::MessageDispatchContext context_;
- };
- } // namespace
- class WebBundleURLLoaderFactoryTest : public ::testing::Test {
- public:
- void SetUp() override {
- mojo::ScopedDataPipeConsumerHandle consumer;
- ASSERT_EQ(CreateDataPipe(nullptr, bundle_data_destination_, consumer),
- MOJO_RESULT_OK);
- mojo::Remote<network::mojom::WebBundleHandle> handle;
- handle_ = std::make_unique<TestWebBundleHandle>(
- handle.BindNewPipeAndPassReceiver());
- devtools_observer_ = std::make_unique<network::MockDevToolsObserver>();
- const network::ResourceRequest::WebBundleTokenParams create_params(
- GURL(kBundleUrl), {} /* token */, {} /* handle */);
- factory_ = std::make_unique<WebBundleURLLoaderFactory>(
- GURL(kBundleUrl), create_params, std::move(handle),
- std::make_unique<MockMemoryQuotaConsumer>(), devtools_observer_->Bind(),
- kBundleRequestId, network::CrossOriginEmbedderPolicy(),
- nullptr /* coep_reporter */);
- factory_->SetBundleStream(std::move(consumer));
- }
- void WriteBundle(base::span<const uint8_t> data) {
- mojo::BlockingCopyFromString(
- std::string(reinterpret_cast<const char*>(data.data()), data.size()),
- bundle_data_destination_);
- }
- void FinishWritingBundle() { bundle_data_destination_.reset(); }
- struct StartRequestResult {
- mojo::Remote<network::mojom::URLLoader> loader;
- std::unique_ptr<network::TestURLLoaderClient> client;
- };
- network::ResourceRequest CreateRequest(
- const GURL& url,
- const std::string& devtools_request_id) {
- network::ResourceRequest request;
- request.url = url;
- request.method = "GET";
- request.request_initiator = url::Origin::Create(GURL(kInitiatorUrl));
- request.web_bundle_token_params =
- network::ResourceRequest::WebBundleTokenParams();
- request.web_bundle_token_params->bundle_url = GURL(kBundleUrl);
- request.devtools_request_id = devtools_request_id;
- return request;
- }
- StartRequestResult StartRequest(const network::ResourceRequest& request) {
- StartRequestResult result;
- result.client = std::make_unique<network::TestURLLoaderClient>();
- factory_->StartSubresourceRequest(
- result.loader.BindNewPipeAndPassReceiver(), request,
- result.client->CreateRemote(),
- mojo::Remote<network::mojom::TrustedHeaderClient>(), base::Time::Now(),
- base::TimeTicks::Now());
- return result;
- }
- StartRequestResult StartRequest(const GURL& url,
- const std::string& devtools_request_id) {
- return StartRequest(CreateRequest(url, devtools_request_id));
- }
- void RunUntilBundleError() { handle_->RunUntilBundleError(); }
- const absl::optional<
- std::pair<network::mojom::WebBundleErrorType, std::string>>&
- last_bundle_error() const {
- return handle_->last_bundle_error();
- }
- protected:
- std::unique_ptr<network::MockDevToolsObserver> devtools_observer_;
- std::unique_ptr<WebBundleURLLoaderFactory> factory_;
- private:
- std::unique_ptr<TestWebBundleHandle> handle_;
- mojo::ScopedDataPipeProducerHandle bundle_data_destination_;
- base::test::TaskEnvironment task_environment;
- };
- TEST_F(WebBundleURLLoaderFactoryTest, Basic) {
- base::HistogramTester histogram_tester;
- WriteBundle(CreateSmallBundle());
- FinishWritingBundle();
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleMetadata(kBundleRequestId,
- ElementsAre(GURL(kResourceUrl))));
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleInnerResponse(
- kResourceRequestId, GURL(kResourceUrl),
- Optional(std::string(kBundleRequestId))));
- auto request = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- request.client->RunUntilComplete();
- EXPECT_EQ(net::OK, request.client->completion_status().error_code);
- EXPECT_FALSE(last_bundle_error().has_value());
- EXPECT_EQ(request.client->response_head()->web_bundle_url, GURL(kBundleUrl));
- std::string body;
- EXPECT_TRUE(mojo::BlockingCopyToString(
- request.client->response_body_release(), &body));
- EXPECT_EQ("body", body);
- histogram_tester.ExpectUniqueSample(
- "SubresourceWebBundles.LoadResult",
- WebBundleURLLoaderFactory::SubresourceWebBundleLoadResult::kSuccess, 1);
- }
- TEST_F(WebBundleURLLoaderFactoryTest, MetadataParseError) {
- base::HistogramTester histogram_tester;
- auto request = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- std::vector<uint8_t> bundle = CreateSmallBundle();
- bundle[4] ^= 1; // Mutate magic bytes.
- WriteBundle(bundle);
- FinishWritingBundle();
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleMetadataError(kBundleRequestId,
- Eq("Wrong magic bytes.")));
- EXPECT_CALL(*devtools_observer_, OnSubresourceWebBundleInnerResponse(_, _, _))
- .Times(0);
- request.client->RunUntilComplete();
- RunUntilBundleError();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request.client->completion_status().error_code);
- EXPECT_EQ(last_bundle_error()->first,
- network::mojom::WebBundleErrorType::kMetadataParseError);
- EXPECT_EQ(last_bundle_error()->second, "Wrong magic bytes.");
- // Requests made after metadata parse error should also fail.
- auto request2 = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- request2.client->RunUntilComplete();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request2.client->completion_status().error_code);
- histogram_tester.ExpectUniqueSample(
- "SubresourceWebBundles.LoadResult",
- WebBundleURLLoaderFactory::SubresourceWebBundleLoadResult::
- kMetadataParseError,
- 1);
- }
- TEST_F(WebBundleURLLoaderFactoryTest, MetadataWithInvalidExchangeUrl) {
- base::HistogramTester histogram_tester;
- auto request = StartRequest(GURL(kInvalidResourceUrl), kResourceRequestId);
- web_package::WebBundleBuilder builder;
- builder.AddExchange(kInvalidResourceUrl,
- {{":status", "200"}, {"content-type", "text/plain"}},
- "body");
- WriteBundle(builder.CreateBundle());
- FinishWritingBundle();
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleMetadataError(
- kBundleRequestId, Eq("Exchange URL is not valid.")));
- EXPECT_CALL(*devtools_observer_, OnSubresourceWebBundleInnerResponse(_, _, _))
- .Times(0);
- request.client->RunUntilComplete();
- RunUntilBundleError();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request.client->completion_status().error_code);
- EXPECT_EQ(last_bundle_error()->first,
- network::mojom::WebBundleErrorType::kMetadataParseError);
- EXPECT_EQ(last_bundle_error()->second, "Exchange URL is not valid.");
- // Requests made after metadata parse error should also fail.
- auto request2 = StartRequest(GURL(kInvalidResourceUrl), kResourceRequestId);
- request2.client->RunUntilComplete();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request2.client->completion_status().error_code);
- histogram_tester.ExpectUniqueSample(
- "SubresourceWebBundles.LoadResult",
- WebBundleURLLoaderFactory::SubresourceWebBundleLoadResult::
- kMetadataParseError,
- 1);
- }
- TEST_F(WebBundleURLLoaderFactoryTest, ResponseParseError) {
- web_package::WebBundleBuilder builder;
- // An invalid response.
- builder.AddExchange(kResourceUrl, {{":status", "0"}}, "body");
- WriteBundle(builder.CreateBundle());
- FinishWritingBundle();
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleMetadata(kBundleRequestId,
- ElementsAre(GURL(kResourceUrl))));
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleInnerResponseError(
- kResourceRequestId, GURL(kResourceUrl),
- Eq(":status must be 3 ASCII decimal digits."),
- Optional(std::string(kBundleRequestId))));
- auto request = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- request.client->RunUntilComplete();
- RunUntilBundleError();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request.client->completion_status().error_code);
- EXPECT_EQ(last_bundle_error()->first,
- network::mojom::WebBundleErrorType::kResponseParseError);
- EXPECT_EQ(last_bundle_error()->second,
- ":status must be 3 ASCII decimal digits.");
- }
- TEST_F(WebBundleURLLoaderFactoryTest, ResourceNotFoundInBundle) {
- WriteBundle(CreateSmallBundle());
- FinishWritingBundle();
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleMetadata(kBundleRequestId,
- ElementsAre(GURL(kResourceUrl))));
- EXPECT_CALL(*devtools_observer_, OnSubresourceWebBundleInnerResponse(_, _, _))
- .Times(0);
- auto request = StartRequest(GURL("https://example.com/no-such-resource"),
- kResourceRequestId);
- request.client->RunUntilComplete();
- RunUntilBundleError();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request.client->completion_status().error_code);
- EXPECT_EQ(last_bundle_error()->first,
- network::mojom::WebBundleErrorType::kResourceNotFound);
- EXPECT_EQ(
- last_bundle_error()->second,
- "https://example.com/no-such-resource is not found in the WebBundle.");
- }
- TEST_F(WebBundleURLLoaderFactoryTest, RedirectResponseIsNotAllowed) {
- web_package::WebBundleBuilder builder;
- builder.AddExchange(kResourceUrl,
- {{":status", "301"}, {"location", kResourceUrl2}}, "");
- builder.AddExchange(kResourceUrl2,
- {{":status", "200"}, {"content-type", "text/plain"}},
- "body");
- WriteBundle(builder.CreateBundle());
- FinishWritingBundle();
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleMetadata(
- kBundleRequestId,
- ElementsAre(GURL(kResourceUrl), GURL(kResourceUrl2))));
- EXPECT_CALL(*devtools_observer_,
- OnSubresourceWebBundleInnerResponse(
- kResourceRequestId, GURL(kResourceUrl),
- Optional(std::string(kBundleRequestId))));
- auto request = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- request.client->RunUntilComplete();
- RunUntilBundleError();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request.client->completion_status().error_code);
- EXPECT_EQ(last_bundle_error()->first,
- network::mojom::WebBundleErrorType::kResponseParseError);
- EXPECT_EQ(last_bundle_error()->second, "Invalid response code 301");
- }
- TEST_F(WebBundleURLLoaderFactoryTest, StartRequestBeforeReadingBundle) {
- auto request = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- WriteBundle(CreateSmallBundle());
- FinishWritingBundle();
- request.client->RunUntilComplete();
- EXPECT_EQ(net::OK, request.client->completion_status().error_code);
- }
- TEST_F(WebBundleURLLoaderFactoryTest, MultipleRequests) {
- auto request1 = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- auto request2 = StartRequest(GURL(kResourceUrl2), kResourceRequestId2);
- std::vector<uint8_t> bundle = CreateLargeBundle();
- // Write the first 10kB of the bundle in which the bundle's metadata and the
- // response for kResourceUrl are included.
- ASSERT_GT(bundle.size(), 10000U);
- WriteBundle(base::make_span(bundle).subspan(0, 10000));
- request1.client->RunUntilComplete();
- EXPECT_EQ(net::OK, request1.client->completion_status().error_code);
- EXPECT_FALSE(request2.client->has_received_completion());
- // Write the rest of the data.
- WriteBundle(base::make_span(bundle).subspan(10000));
- FinishWritingBundle();
- request2.client->RunUntilComplete();
- EXPECT_EQ(net::OK, request2.client->completion_status().error_code);
- }
- TEST_F(WebBundleURLLoaderFactoryTest, CancelRequest) {
- auto request_to_complete1 =
- StartRequest(GURL(kResourceUrl), kResourceRequestId);
- auto request_to_complete2 =
- StartRequest(GURL(kResourceUrl2), kResourceRequestId2);
- auto request_to_cancel1 =
- StartRequest(GURL(kResourceUrl), kResourceRequestId);
- auto request_to_cancel2 =
- StartRequest(GURL(kResourceUrl2), kResourceRequestId2);
- auto request_to_cancel3 =
- StartRequest(GURL(kResourceUrl3), kResourceRequestId3);
- // Cancel request before getting metadata.
- request_to_cancel1.loader.reset();
- std::vector<uint8_t> bundle = CreateLargeBundle();
- // Write the first 10kB of the bundle in which the bundle's metadata, response
- // for kResourceUrl, and response header for kResourceUrl2 are included.
- ASSERT_GT(bundle.size(), 10000U);
- WriteBundle(base::make_span(bundle).subspan(0, 10000));
- // This makes sure the bytes written above are consumed by WebBundle parser.
- request_to_complete1.client->RunUntilComplete();
- // Cancel request after reading response header, but before reading body.
- request_to_cancel2.loader.reset();
- // Cancel request after getting metadata, but before reading response header.
- request_to_cancel3.loader.reset();
- // Write the rest of the data.
- WriteBundle(base::make_span(bundle).subspan(10000));
- FinishWritingBundle();
- request_to_complete2.client->RunUntilComplete();
- EXPECT_EQ(net::OK,
- request_to_complete2.client->completion_status().error_code);
- }
- TEST_F(WebBundleURLLoaderFactoryTest,
- FactoryDestructionCancelsInflightRequests) {
- auto request = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- factory_ = nullptr;
- WriteBundle(CreateSmallBundle());
- FinishWritingBundle();
- request.client->RunUntilComplete();
- EXPECT_EQ(net::ERR_FAILED, request.client->completion_status().error_code);
- }
- TEST_F(WebBundleURLLoaderFactoryTest, TruncatedBundle) {
- std::vector<uint8_t> bundle = CreateSmallBundle();
- // Truncate in the middle of responses section.
- bundle.resize(bundle.size() - 10);
- WriteBundle(std::move(bundle));
- FinishWritingBundle();
- auto request = StartRequest(GURL(kResourceUrl), kResourceRequestId);
- request.client->RunUntilComplete();
- RunUntilBundleError();
- EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
- request.client->completion_status().error_code);
- EXPECT_EQ(last_bundle_error()->first,
- network::mojom::WebBundleErrorType::kResponseParseError);
- EXPECT_EQ(last_bundle_error()->second, "Error reading response header.");
- }
- TEST_F(WebBundleURLLoaderFactoryTest, CrossOriginJson) {
- WriteBundle(CreateCrossOriginBundle());
- FinishWritingBundle();
- auto request = StartRequest(GURL(kCrossOriginJsonUrl), kResourceRequestId);
- request.client->RunUntilComplete();
- EXPECT_EQ(net::OK, request.client->completion_status().error_code);
- EXPECT_FALSE(last_bundle_error().has_value());
- std::string body;
- ASSERT_TRUE(mojo::BlockingCopyToString(
- request.client->response_body_release(), &body));
- EXPECT_TRUE(body.empty())
- << "body should be empty because JSON is a CORB-protected resource";
- }
- TEST_F(WebBundleURLLoaderFactoryTest, CrossOriginJs) {
- WriteBundle(CreateCrossOriginBundle());
- FinishWritingBundle();
- auto request = StartRequest(GURL(kCrossOriginJsUrl), kResourceRequestId);
- request.client->RunUntilComplete();
- EXPECT_EQ(net::OK, request.client->completion_status().error_code);
- EXPECT_FALSE(last_bundle_error().has_value());
- std::string body;
- ASSERT_TRUE(mojo::BlockingCopyToString(
- request.client->response_body_release(), &body));
- EXPECT_EQ("const not_secret = 1;", body)
- << "body should be valid one because JS is not a CORB protected resource";
- }
- TEST_F(WebBundleURLLoaderFactoryTest, WrongBundleURL) {
- BadMessageTestHelper bad_message_helper;
- WriteBundle(CreateSmallBundle());
- FinishWritingBundle();
- network::ResourceRequest url_request =
- CreateRequest(GURL(kResourceUrl), kResourceRequestId);
- url_request.web_bundle_token_params->bundle_url =
- GURL("https://modified-bundle-url.example.com/");
- auto request = StartRequest(url_request);
- request.client->RunUntilComplete();
- EXPECT_EQ(net::ERR_INVALID_ARGUMENT,
- request.client->completion_status().error_code);
- EXPECT_THAT(bad_message_helper.bad_message_reports(),
- ::testing::ElementsAre(
- "WebBundleURLLoaderFactory: Bundle URL does not match"));
- }
- } // namespace web_package
|