NetworkErrorLoggingService: Load from and write to a PersistentNELStore

This CL implements the interface between NetworkErrorLoggingServiceImpl
and its PersistentNELStore. If a store is provided upon construction,
The NetworkErrorLoggingService will attempt to load all NEL policies
from the store on the first call to OnHeader(), OnRequest(),
QueueSignedExchangeReport(), RemoveBrowsingData(), or
RemoveAllBrowsingData(). Until the store notifies us that it is
done loading, calls to those public methods will be queued in a backlog
to be run in order upon completion of loading. Any changes to the NEL
policies stored in memory will be reflected in the PersistentNELStore.

Bug: 895821
Change-Id: Ib9a18a2b6c73641ee0a3d2f7b119b4c710c4db5b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1555242
Commit-Queue: Lily Chen <[email protected]>
Reviewed-by: Eric Orth <[email protected]>
Reviewed-by: Martin Šrámek <[email protected]>
Cr-Commit-Position: refs/heads/master@{#654452}
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
index f3f9e679..376b6ee5 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
@@ -1100,8 +1100,7 @@
 
   void OnRequest(RequestDetails details) override { NOTREACHED(); }
 
-  void QueueSignedExchangeReport(
-      const SignedExchangeReportDetails& details) override {
+  void QueueSignedExchangeReport(SignedExchangeReportDetails details) override {
     NOTREACHED();
   }
 
diff --git a/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc b/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc
index a2adb6b..c43aaee 100644
--- a/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc
+++ b/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc
@@ -60,6 +60,7 @@
 #include "url/origin.h"
 
 #if BUILDFLAG(ENABLE_REPORTING)
+#include "net/network_error_logging/mock_persistent_nel_store.h"
 #include "net/network_error_logging/network_error_logging_service.h"
 #include "net/reporting/reporting_cache.h"
 #include "net/reporting/reporting_report.h"
@@ -1360,9 +1361,44 @@
                                 BrowsingDataRemover::DATA_TYPE_COOKIES, false);
 }
 
-// TODO(chlily): Use a PersistentNELStore and test that entries are removed from
-// it.
 TEST_F(BrowsingDataRemoverImplTest, RemoveNetworkErrorLogging) {
+  auto store = std::make_unique<net::MockPersistentNELStore>();
+  std::unique_ptr<net::NetworkErrorLoggingService> logging_service =
+      net::NetworkErrorLoggingService::Create(store.get());
+  BrowserContext::GetDefaultStoragePartition(GetBrowserContext())
+      ->GetURLRequestContext()
+      ->GetURLRequestContext()
+      ->set_network_error_logging_service(logging_service.get());
+
+  GURL domain("https://google.com");
+  logging_service->OnHeader(url::Origin::Create(domain),
+                            net::IPAddress(192, 168, 0, 1),
+                            "{\"report_to\":\"group\",\"max_age\":86400}");
+  store->FinishLoading(true /* load_success */);
+  store->Flush();
+
+  ASSERT_EQ(1u, logging_service->GetPolicyOriginsForTesting().size());
+  ASSERT_EQ(1, store->StoredPoliciesCount());
+
+  BlockUntilBrowsingDataRemoved(base::Time(), base::Time::Max(),
+                                BrowsingDataRemover::DATA_TYPE_COOKIES, false);
+
+  EXPECT_TRUE(logging_service->GetPolicyOriginsForTesting().empty());
+  EXPECT_EQ(0, store->StoredPoliciesCount());
+
+  // Check that the persistent store was told to delete the policy.
+  net::NetworkErrorLoggingService::NELPolicy deleted_policy;
+  deleted_policy.origin = url::Origin::Create(domain);
+  EXPECT_THAT(store->GetAllCommands(),
+              testing::Contains(net::MockPersistentNELStore::Command(
+                  net::MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY,
+                  deleted_policy)));
+}
+
+// Test that removal of in-memory browsing data works without a persistent
+// store.
+TEST_F(BrowsingDataRemoverImplTest,
+       RemoveNetworkErrorLogging_NoPersistentStore) {
   std::unique_ptr<net::NetworkErrorLoggingService> logging_service =
       net::NetworkErrorLoggingService::Create(nullptr /* store */);
   BrowserContext::GetDefaultStoragePartition(GetBrowserContext())
@@ -1383,11 +1419,10 @@
   EXPECT_TRUE(logging_service->GetPolicyOriginsForTesting().empty());
 }
 
-// TODO(chlily): Use a PersistentNELStore and test that entries are removed from
-// it.
 TEST_F(BrowsingDataRemoverImplTest, RemoveNetworkErrorLogging_SpecificOrigins) {
+  auto store = std::make_unique<net::MockPersistentNELStore>();
   std::unique_ptr<net::NetworkErrorLoggingService> logging_service =
-      net::NetworkErrorLoggingService::Create(nullptr /* store */);
+      net::NetworkErrorLoggingService::Create(store.get());
   BrowserContext::GetDefaultStoragePartition(GetBrowserContext())
       ->GetURLRequestContext()
       ->GetURLRequestContext()
@@ -1409,8 +1444,11 @@
   logging_service->OnHeader(url::Origin::Create(domain4),
                             net::IPAddress(192, 168, 0, 1),
                             "{\"report_to\":\"group\",\"max_age\":86400}");
+  store->FinishLoading(true /* load_success */);
+  store->Flush();
 
   ASSERT_EQ(4u, logging_service->GetPolicyOriginsForTesting().size());
+  ASSERT_EQ(4, store->StoredPoliciesCount());
 
   std::unique_ptr<BrowsingDataFilterBuilder> filter_builder(
       BrowsingDataFilterBuilder::Create(BrowsingDataFilterBuilder::WHITELIST));
@@ -1426,6 +1464,21 @@
   EXPECT_THAT(policy_origins,
               UnorderedElementsAre(url::Origin::Create(domain2),
                                    url::Origin::Create(domain4)));
+  EXPECT_EQ(2, store->StoredPoliciesCount());
+
+  // Check that the persistent store was told to delete the policies.
+  net::NetworkErrorLoggingService::NELPolicy deleted_policy1;
+  deleted_policy1.origin = url::Origin::Create(domain1);
+  net::NetworkErrorLoggingService::NELPolicy deleted_policy2;
+  deleted_policy2.origin = url::Origin::Create(domain3);
+  EXPECT_THAT(store->GetAllCommands(),
+              testing::Contains(net::MockPersistentNELStore::Command(
+                  net::MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY,
+                  deleted_policy1)));
+  EXPECT_THAT(store->GetAllCommands(),
+              testing::Contains(net::MockPersistentNELStore::Command(
+                  net::MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY,
+                  deleted_policy2)));
 }
 
 TEST_F(BrowsingDataRemoverImplTest, RemoveNetworkErrorLogging_NoService) {
diff --git a/net/extras/sqlite/sqlite_persistent_reporting_and_nel_store_unittest.cc b/net/extras/sqlite/sqlite_persistent_reporting_and_nel_store_unittest.cc
index c61007d9..bab431d 100644
--- a/net/extras/sqlite/sqlite_persistent_reporting_and_nel_store_unittest.cc
+++ b/net/extras/sqlite/sqlite_persistent_reporting_and_nel_store_unittest.cc
@@ -15,8 +15,11 @@
 #include "base/synchronization/waitable_event.h"
 #include "base/task/post_task.h"
 #include "base/test/bind_test_util.h"
+#include "base/test/simple_test_clock.h"
 #include "base/threading/thread_restrictions.h"
 #include "base/threading/thread_task_runner_handle.h"
+#include "net/network_error_logging/network_error_logging_service.h"
+#include "net/reporting/reporting_test_util.h"
 #include "net/test/test_with_scoped_task_environment.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -27,8 +30,13 @@
 const base::FilePath::CharType kReportingAndNELStoreFilename[] =
     FILE_PATH_LITERAL("ReportingAndNEL");
 
-const url::Origin kOrigin1 = url::Origin::Create(GURL("https://www.foo.test"));
-const url::Origin kOrigin2 = url::Origin::Create(GURL("https://www.bar.test"));
+const GURL kUrl1 = GURL("https://www.foo.test");
+const GURL kUrl2 = GURL("https://www.bar.test");
+const url::Origin kOrigin1 = url::Origin::Create(kUrl1);
+const url::Origin kOrigin2 = url::Origin::Create(kUrl2);
+const IPAddress kServerIP = IPAddress(192, 168, 0, 1);
+const std::string kHeader = "{\"report_to\":\"group\",\"max_age\":86400}";
+const std::string kHeaderMaxAge0 = "{\"report_to\":\"group\",\"max_age\":0}";
 
 enum class Op { kAdd, kDelete, kUpdate };
 
@@ -350,4 +358,216 @@
   RunUntilIdle();
 }
 
+// These tests test that a SQLitePersistentReportingAndNELStore
+// can be used by a NetworkErrorLoggingService to persist NEL policies.
+class SQLitePersistNELTest : public SQLitePersistentReportingAndNELStoreTest {
+ public:
+  SQLitePersistNELTest() {}
+
+  void SetUp() override {
+    SQLitePersistentReportingAndNELStoreTest::SetUp();
+    SetUpNetworkErrorLoggingService();
+  }
+
+  void TearDown() override {
+    service_->OnShutdown();
+    service_.reset();
+    reporting_service_.reset();
+    SQLitePersistentReportingAndNELStoreTest::TearDown();
+  }
+
+  void SetUpNetworkErrorLoggingService() {
+    CreateStore();
+    service_ = NetworkErrorLoggingService::Create(store_.get());
+    reporting_service_ = std::make_unique<TestReportingService>();
+    service_->SetReportingService(reporting_service_.get());
+    service_->SetClockForTesting(&clock_);
+  }
+
+  void SimulateRestart() {
+    TearDown();
+    SetUpNetworkErrorLoggingService();
+  }
+
+  NetworkErrorLoggingService::RequestDetails MakeRequestDetails(
+      GURL url,
+      Error error_type) {
+    NetworkErrorLoggingService::RequestDetails details;
+
+    details.uri = url;
+    details.referrer = GURL("https://referrer.com/");
+    details.user_agent = "Mozilla/1.0";
+    details.server_ip = kServerIP;
+    details.method = "GET";
+    details.status_code = 0;
+    details.elapsed_time = base::TimeDelta::FromSeconds(1);
+    details.type = error_type;
+    details.reporting_upload_depth = 0;
+
+    return details;
+  }
+
+ protected:
+  base::SimpleTestClock clock_;
+  std::unique_ptr<NetworkErrorLoggingService> service_;
+  std::unique_ptr<TestReportingService> reporting_service_;
+};
+
+TEST_F(SQLitePersistNELTest, AddAndRetrieveNELPolicy) {
+  service_->OnHeader(kOrigin1, kServerIP, kHeader);
+  RunUntilIdle();
+
+  EXPECT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+
+  EXPECT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+
+  EXPECT_THAT(reporting_service_->reports(),
+              testing::ElementsAre(ReportUrlIs(kUrl1)));
+}
+
+TEST_F(SQLitePersistNELTest, AddAndDeleteNELPolicy) {
+  service_->OnHeader(kOrigin1, kServerIP, kHeader);
+  RunUntilIdle();
+
+  EXPECT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  SimulateRestart();
+
+  // Deletes the stored policy.
+  service_->OnHeader(kOrigin1, kServerIP, kHeaderMaxAge0);
+  RunUntilIdle();
+
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  EXPECT_EQ(0u, reporting_service_->reports().size());
+}
+
+TEST_F(SQLitePersistNELTest, ExpirationTimeIsPersisted) {
+  service_->OnHeader(kOrigin1, kServerIP, kHeader);
+  RunUntilIdle();
+
+  // Makes the policy we just added expired.
+  clock_.Advance(base::TimeDelta::FromSeconds(86401));
+
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+
+  EXPECT_EQ(0u, reporting_service_->reports().size());
+
+  // Add the policy again so that it is not expired.
+  service_->OnHeader(kOrigin1, kServerIP, kHeader);
+
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+
+  EXPECT_THAT(reporting_service_->reports(),
+              testing::ElementsAre(ReportUrlIs(kUrl1)));
+}
+
+TEST_F(SQLitePersistNELTest, OnRequestUpdatesAccessTime) {
+  service_->OnHeader(kOrigin1, kServerIP, kHeader);
+  RunUntilIdle();
+
+  SimulateRestart();
+
+  // Update the access time by sending a request.
+  clock_.Advance(base::TimeDelta::FromSeconds(100));
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+
+  EXPECT_THAT(reporting_service_->reports(),
+              testing::ElementsAre(ReportUrlIs(kUrl1)));
+
+  SimulateRestart();
+  // Check that the policy's access time has been updated.
+  base::Time now = clock_.Now();
+  NetworkErrorLoggingService::NELPolicy policy = MakeNELPolicy(kOrigin1, now);
+  std::vector<NetworkErrorLoggingService::NELPolicy> policies;
+  LoadNELPolicies(&policies);
+  ASSERT_EQ(1u, policies.size());
+  EXPECT_EQ(policy.origin, policies[0].origin);
+  EXPECT_TRUE(WithinOneMicrosecond(policy.last_used, policies[0].last_used));
+}
+
+TEST_F(SQLitePersistNELTest, RemoveSomeBrowsingData) {
+  service_->OnHeader(kOrigin1, kServerIP, kHeader);
+  service_->OnHeader(kOrigin2, kServerIP, kHeader);
+  RunUntilIdle();
+
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+
+  ASSERT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  ASSERT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin2));
+  EXPECT_THAT(reporting_service_->reports(),
+              testing::ElementsAre(ReportUrlIs(kUrl1)));
+
+  SimulateRestart();
+
+  service_->RemoveBrowsingData(
+      base::BindRepeating([](const GURL& origin) -> bool {
+        return origin.host() == kOrigin1.host();
+      }));
+  RunUntilIdle();
+
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  EXPECT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin2));
+
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  EXPECT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin2));
+  EXPECT_EQ(0u, reporting_service_->reports().size());
+}
+
+TEST_F(SQLitePersistNELTest, RemoveAllBrowsingData) {
+  service_->OnHeader(kOrigin1, kServerIP, kHeader);
+  service_->OnHeader(kOrigin2, kServerIP, kHeader);
+  RunUntilIdle();
+
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  service_->OnRequest(MakeRequestDetails(kUrl2, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+
+  ASSERT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  ASSERT_EQ(1u, service_->GetPolicyOriginsForTesting().count(kOrigin2));
+  EXPECT_THAT(reporting_service_->reports(),
+              testing::ElementsAre(ReportUrlIs(kUrl1), ReportUrlIs(kUrl2)));
+
+  SimulateRestart();
+
+  service_->RemoveAllBrowsingData();
+  RunUntilIdle();
+
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin2));
+
+  SimulateRestart();
+
+  service_->OnRequest(MakeRequestDetails(kUrl1, ERR_INVALID_RESPONSE));
+  service_->OnRequest(MakeRequestDetails(kUrl2, ERR_INVALID_RESPONSE));
+  RunUntilIdle();
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin1));
+  EXPECT_EQ(0u, service_->GetPolicyOriginsForTesting().count(kOrigin2));
+  EXPECT_EQ(0u, reporting_service_->reports().size());
+}
+
 }  // namespace net
diff --git a/net/network_error_logging/network_error_logging_service.cc b/net/network_error_logging/network_error_logging_service.cc
index aa875fb..f3c8af42 100644
--- a/net/network_error_logging/network_error_logging_service.cc
+++ b/net/network_error_logging/network_error_logging_service.cc
@@ -9,6 +9,7 @@
 #include <utility>
 #include <vector>
 
+#include "base/bind.h"
 #include "base/json/json_reader.h"
 #include "base/logging.h"
 #include "base/metrics/histogram_macros.h"
@@ -181,10 +182,16 @@
 class NetworkErrorLoggingServiceImpl : public NetworkErrorLoggingService {
  public:
   explicit NetworkErrorLoggingServiceImpl(PersistentNELStore* store)
-      : store_(store) {}
+      : store_(store),
+        started_loading_policies_(false),
+        initialized_(false),
+        weak_factory_(this) {
+    if (!PoliciesArePersisted())
+      initialized_ = true;
+  }
 
   ~NetworkErrorLoggingServiceImpl() override {
-    if (store_)
+    if (PoliciesArePersisted() && initialized_)
       store_->Flush();
   }
 
@@ -193,9 +200,6 @@
   void OnHeader(const url::Origin& origin,
                 const IPAddress& received_ip_address,
                 const std::string& value) override {
-    if (shut_down_)
-      return;
-
     // NEL is only available to secure origins, so don't permit insecure origins
     // to set policies.
     if (!origin.GetURL().SchemeIsCryptographic()) {
@@ -203,10 +207,184 @@
       return;
     }
 
+    base::Time header_received_time = clock_->Now();
+    // base::Unretained is safe because the callback gets stored in
+    // task_backlog_, so the callback will not outlive |*this|.
+    DoOrBacklogTask(base::BindOnce(
+        &NetworkErrorLoggingServiceImpl::DoOnHeader, base::Unretained(this),
+        origin, received_ip_address, value, header_received_time));
+  }
+
+  void OnRequest(RequestDetails details) override {
+    // This method is only called on secure requests.
+    DCHECK(details.uri.SchemeIsCryptographic());
+
+    if (!reporting_service_) {
+      RecordRequestOutcome(RequestOutcome::kDiscardedNoReportingService);
+      return;
+    }
+
+    base::Time request_received_time = clock_->Now();
+    // base::Unretained is safe because the callback gets stored in
+    // task_backlog_, so the callback will not outlive |*this|.
+    DoOrBacklogTask(base::BindOnce(&NetworkErrorLoggingServiceImpl::DoOnRequest,
+                                   base::Unretained(this), std::move(details),
+                                   request_received_time));
+  }
+
+  void QueueSignedExchangeReport(SignedExchangeReportDetails details) override {
+    if (!reporting_service_) {
+      RecordSignedExchangeRequestOutcome(
+          RequestOutcome::kDiscardedNoReportingService);
+      return;
+    }
+    if (!details.outer_url.SchemeIsCryptographic()) {
+      RecordSignedExchangeRequestOutcome(
+          RequestOutcome::kDiscardedInsecureOrigin);
+      return;
+    }
+
+    base::Time request_received_time = clock_->Now();
+    // base::Unretained is safe because the callback gets stored in
+    // task_backlog_, so the callback will not outlive |*this|.
+    DoOrBacklogTask(base::BindOnce(
+        &NetworkErrorLoggingServiceImpl::DoQueueSignedExchangeReport,
+        base::Unretained(this), std::move(details), request_received_time));
+  }
+
+  void RemoveBrowsingData(const base::RepeatingCallback<bool(const GURL&)>&
+                              origin_filter) override {
+    // base::Unretained is safe because the callback gets stored in
+    // task_backlog_, so the callback will not outlive |*this|.
+    DoOrBacklogTask(
+        base::BindOnce(&NetworkErrorLoggingServiceImpl::DoRemoveBrowsingData,
+                       base::Unretained(this), origin_filter));
+  }
+
+  void RemoveAllBrowsingData() override {
+    // base::Unretained is safe because the callback gets stored in
+    // task_backlog_, so the callback will not outlive |*this|.
+    DoOrBacklogTask(
+        base::BindOnce(&NetworkErrorLoggingServiceImpl::DoRemoveAllBrowsingData,
+                       base::Unretained(this)));
+  }
+
+  base::Value StatusAsValue() const override {
+    base::Value dict(base::Value::Type::DICTIONARY);
+    std::vector<base::Value> policy_list;
+    // We wanted sorted (or at least reproducible) output; luckily, policies_ is
+    // a std::map, and therefore already sorted.
+    for (const auto& origin_and_policy : policies_) {
+      const auto& origin = origin_and_policy.first;
+      const auto& policy = origin_and_policy.second;
+      base::Value policy_dict(base::Value::Type::DICTIONARY);
+      policy_dict.SetKey("origin", base::Value(origin.Serialize()));
+      policy_dict.SetKey("includeSubdomains",
+                         base::Value(policy.include_subdomains));
+      policy_dict.SetKey("reportTo", base::Value(policy.report_to));
+      policy_dict.SetKey("expires",
+                         base::Value(NetLog::TimeToString(policy.expires)));
+      policy_dict.SetKey("successFraction",
+                         base::Value(policy.success_fraction));
+      policy_dict.SetKey("failureFraction",
+                         base::Value(policy.failure_fraction));
+      policy_list.push_back(std::move(policy_dict));
+    }
+    dict.SetKey("originPolicies", base::Value(std::move(policy_list)));
+    return dict;
+  }
+
+  std::set<url::Origin> GetPolicyOriginsForTesting() override {
+    std::set<url::Origin> origins;
+    for (const auto& entry : policies_) {
+      origins.insert(entry.first);
+    }
+    return origins;
+  }
+
+ private:
+  // Map from origin to origin's (owned) policy.
+  // Would be unordered_map, but url::Origin has no hash.
+  using PolicyMap = std::map<url::Origin, NELPolicy>;
+
+  // Wildcard policies are policies for which the include_subdomains flag is
+  // set.
+  //
+  // Wildcard policies are accessed by domain name, not full origin, so there
+  // can be multiple wildcard policies per domain name.
+  //
+  // This is a map from domain name to the set of pointers to wildcard policies
+  // in that domain.
+  //
+  // Policies in the map are unowned; they are pointers to the original in the
+  // PolicyMap.
+  using WildcardPolicyMap = std::map<std::string, std::set<const NELPolicy*>>;
+
+  PolicyMap policies_;
+  WildcardPolicyMap wildcard_policies_;
+
+  // The persistent store in which NEL policies will be stored to disk, if not
+  // null. If |store_| is null, then NEL policies will be in-memory only.
+  // The store is owned by the URLRequestContext because Reporting also needs
+  // access to it.
+  PersistentNELStore* store_;
+
+  // Set to true when we have told the store to load NEL policies. This is to
+  // make sure we don't try to load policies multiple times.
+  bool started_loading_policies_;
+
+  // Set to true when the NEL service has been initialized. Before
+  // initialization is complete, commands to the NEL service (i.e. public
+  // method calls) are stashed away in |task_backlog_|, to be executed once
+  // initialization is complete. Initialization is complete automatically if
+  // there is no PersistentNELStore. If there is a store, then initialization is
+  // complete when the NEL policies have finished being loaded from the store
+  // (either successfully or unsuccessfully).
+  bool initialized_;
+
+  // Backlog of tasks waiting on initialization.
+  std::vector<base::OnceClosure> task_backlog_;
+
+  base::WeakPtrFactory<NetworkErrorLoggingServiceImpl> weak_factory_;
+
+  bool PoliciesArePersisted() const { return store_ != nullptr; }
+
+  void DoOrBacklogTask(base::OnceClosure task) {
+    if (shut_down_)
+      return;
+
+    FetchAllPoliciesFromStoreIfNecessary();
+
+    if (!initialized_) {
+      task_backlog_.push_back(std::move(task));
+      return;
+    }
+
+    std::move(task).Run();
+  }
+
+  void ExecuteBacklog() {
+    DCHECK(initialized_);
+
+    if (shut_down_)
+      return;
+
+    for (base::OnceClosure& task : task_backlog_) {
+      std::move(task).Run();
+    }
+    task_backlog_.clear();
+  }
+
+  void DoOnHeader(const url::Origin& origin,
+                  const IPAddress& received_ip_address,
+                  const std::string& value,
+                  base::Time header_received_time) {
+    DCHECK(initialized_);
+
     NELPolicy policy;
     policy.origin = origin;
     policy.received_ip_address = received_ip_address;
-    policy.last_used = clock_->Now();
+    policy.last_used = header_received_time;
     HeaderOutcome outcome = ParseHeader(value, clock_->Now(), &policy);
     RecordHeaderOutcome(outcome);
     if (outcome != HeaderOutcome::SET && outcome != HeaderOutcome::REMOVED)
@@ -224,9 +402,7 @@
       return;
 
     DVLOG(1) << "Received NEL policy for " << origin;
-    auto inserted = policies_.insert(std::make_pair(origin, policy));
-    DCHECK(inserted.second);
-    MaybeAddWildcardPolicy(origin, &inserted.first->second);
+    AddPolicy(std::move(policy));
 
     // Evict policies if the policy limit is exceeded.
     if (policies_.size() > kMaxPolicies) {
@@ -237,17 +413,9 @@
     }
   }
 
-  void OnRequest(RequestDetails details) override {
-    if (shut_down_)
-      return;
-
-    if (!reporting_service_) {
-      RecordRequestOutcome(RequestOutcome::kDiscardedNoReportingService);
-      return;
-    }
-
-    // This method is only called on secure requests.
-    DCHECK(details.uri.SchemeIsCryptographic());
+  void DoOnRequest(RequestDetails details, base::Time request_received_time) {
+    DCHECK(reporting_service_);
+    DCHECK(initialized_);
 
     auto report_origin = url::Origin::Create(details.uri);
     const NELPolicy* policy = FindPolicyForOrigin(report_origin);
@@ -256,8 +424,7 @@
       return;
     }
 
-    // Mark the policy used.
-    policy->last_used = clock_->Now();
+    MarkPolicyUsed(policy, request_received_time);
 
     Error type = details.type;
     // It is expected for Reporting uploads to terminate with ERR_ABORTED, since
@@ -329,21 +496,10 @@
     RecordRequestOutcome(RequestOutcome::kQueued);
   }
 
-  void QueueSignedExchangeReport(
-      const SignedExchangeReportDetails& details) override {
-    if (shut_down_)
-      return;
+  void DoQueueSignedExchangeReport(SignedExchangeReportDetails details,
+                                   base::Time request_received_time) {
+    DCHECK(reporting_service_);
 
-    if (!reporting_service_) {
-      RecordSignedExchangeRequestOutcome(
-          RequestOutcome::kDiscardedNoReportingService);
-      return;
-    }
-    if (!details.outer_url.SchemeIsCryptographic()) {
-      RecordSignedExchangeRequestOutcome(
-          RequestOutcome::kDiscardedInsecureOrigin);
-      return;
-    }
     const auto report_origin = url::Origin::Create(details.outer_url);
     const NELPolicy* policy = FindPolicyForOrigin(report_origin);
     if (!policy) {
@@ -352,8 +508,7 @@
       return;
     }
 
-    // Mark the policy used.
-    policy->last_used = clock_->Now();
+    MarkPolicyUsed(policy, request_received_time);
 
     if (IsMismatchingSubdomainReport(*policy, report_origin)) {
       RecordSignedExchangeRequestOutcome(
@@ -386,8 +541,9 @@
     RecordSignedExchangeRequestOutcome(RequestOutcome::kQueued);
   }
 
-  void RemoveBrowsingData(const base::RepeatingCallback<bool(const GURL&)>&
-                              origin_filter) override {
+  void DoRemoveBrowsingData(
+      const base::RepeatingCallback<bool(const GURL&)>& origin_filter) {
+    DCHECK(initialized_);
     for (auto it = policies_.begin(); it != policies_.end();) {
       const url::Origin& origin = it->first;
       // Remove policies matching the filter.
@@ -397,74 +553,24 @@
         ++it;
       }
     }
+    if (PoliciesArePersisted())
+      store_->Flush();
   }
 
-  void RemoveAllBrowsingData() override {
+  void DoRemoveAllBrowsingData() {
+    DCHECK(initialized_);
+    if (PoliciesArePersisted()) {
+      // TODO(chlily): Add a DeleteAllNELPolicies command to PersistentNELStore.
+      for (auto origin_and_policy : policies_) {
+        store_->DeleteNELPolicy(origin_and_policy.second);
+      }
+      store_->Flush();
+    }
+
     wildcard_policies_.clear();
     policies_.clear();
   }
 
-  base::Value StatusAsValue() const override {
-    base::Value dict(base::Value::Type::DICTIONARY);
-    std::vector<base::Value> policy_list;
-    // We wanted sorted (or at least reproducible) output; luckily, policies_ is
-    // a std::map, and therefore already sorted.
-    for (const auto& origin_and_policy : policies_) {
-      const auto& origin = origin_and_policy.first;
-      const auto& policy = origin_and_policy.second;
-      base::Value policy_dict(base::Value::Type::DICTIONARY);
-      policy_dict.SetKey("origin", base::Value(origin.Serialize()));
-      policy_dict.SetKey("includeSubdomains",
-                         base::Value(policy.include_subdomains));
-      policy_dict.SetKey("reportTo", base::Value(policy.report_to));
-      policy_dict.SetKey("expires",
-                         base::Value(NetLog::TimeToString(policy.expires)));
-      policy_dict.SetKey("successFraction",
-                         base::Value(policy.success_fraction));
-      policy_dict.SetKey("failureFraction",
-                         base::Value(policy.failure_fraction));
-      policy_list.push_back(std::move(policy_dict));
-    }
-    dict.SetKey("originPolicies", base::Value(std::move(policy_list)));
-    return dict;
-  }
-
-  std::set<url::Origin> GetPolicyOriginsForTesting() override {
-    std::set<url::Origin> origins;
-    for (const auto& entry : policies_) {
-      origins.insert(entry.first);
-    }
-    return origins;
-  }
-
- private:
-  // Map from origin to origin's (owned) policy.
-  // Would be unordered_map, but url::Origin has no hash.
-  using PolicyMap = std::map<url::Origin, NELPolicy>;
-
-  // Wildcard policies are policies for which the include_subdomains flag is
-  // set.
-  //
-  // Wildcard policies are accessed by domain name, not full origin, so there
-  // can be multiple wildcard policies per domain name.
-  //
-  // This is a map from domain name to the set of pointers to wildcard policies
-  // in that domain.
-  //
-  // Policies in the map are unowned; they are pointers to the original in the
-  // PolicyMap.
-  using WildcardPolicyMap = std::map<std::string, std::set<const NELPolicy*>>;
-
-  PolicyMap policies_;
-  WildcardPolicyMap wildcard_policies_;
-
-  // The persistent store in which NEL policies will be stored to disk, if not
-  // null. If |store_| is null, then NEL policies will be in-memory only.
-  // The store is owned by the URLRequestContext because Reporting also needs
-  // access to it.
-  // TODO(chlily): Implement.
-  PersistentNELStore* store_;
-
   HeaderOutcome ParseHeader(const std::string& json_value,
                             base::Time now,
                             NELPolicy* policy_out) const {
@@ -529,6 +635,8 @@
   }
 
   const NELPolicy* FindPolicyForOrigin(const url::Origin& origin) const {
+    DCHECK(initialized_);
+
     auto it = policies_.find(origin);
     if (it != policies_.end() && clock_->Now() < it->second.expires)
       return &it->second;
@@ -568,6 +676,25 @@
     return nullptr;
   }
 
+  // There must be no pre-existing policy for |policy.origin|. Returns iterator
+  // to the inserted policy.
+  PolicyMap::iterator AddPolicy(NELPolicy policy) {
+    // If |initialized_| is false, then we are calling this from
+    // OnPoliciesLoaded(), which means we don't want to add the given policy to
+    // the store because we have just loaded it from there.
+    if (PoliciesArePersisted() && initialized_)
+      store_->AddNELPolicy(policy);
+
+    auto iter_and_result =
+        policies_.insert(std::make_pair(policy.origin, std::move(policy)));
+    DCHECK(iter_and_result.second);
+
+    const NELPolicy& inserted_policy = iter_and_result.first->second;
+    MaybeAddWildcardPolicy(inserted_policy.origin, &inserted_policy);
+
+    return iter_and_result.first;
+  }
+
   void MaybeAddWildcardPolicy(const url::Origin& origin,
                               const NELPolicy* policy) {
     DCHECK(policy);
@@ -586,6 +713,10 @@
     DCHECK(policy_it != policies_.end());
     NELPolicy* policy = &policy_it->second;
     MaybeRemoveWildcardPolicy(policy);
+
+    if (PoliciesArePersisted() && initialized_)
+      store_->DeleteNELPolicy(*policy);
+
     return policies_.erase(policy_it);
   }
 
@@ -607,6 +738,12 @@
       wildcard_policies_.erase(wildcard_it);
   }
 
+  void MarkPolicyUsed(const NELPolicy* policy, base::Time time_used) const {
+    policy->last_used = time_used;
+    if (PoliciesArePersisted() && initialized_)
+      store_->UpdateNELPolicyAccessTime(*policy);
+  }
+
   void RemoveAllExpiredPolicies() {
     for (auto it = policies_.begin(); it != policies_.end();) {
       if (it->second.expires < clock_->Now()) {
@@ -701,6 +838,41 @@
       return base::nullopt;
     return sampling_fraction;
   }
+
+  void FetchAllPoliciesFromStoreIfNecessary() {
+    if (!PoliciesArePersisted() || started_loading_policies_)
+      return;
+
+    started_loading_policies_ = true;
+    FetchAllPoliciesFromStore();
+  }
+
+  void FetchAllPoliciesFromStore() {
+    DCHECK(PoliciesArePersisted());
+    DCHECK(!initialized_);
+
+    store_->LoadNELPolicies(
+        base::BindOnce(&NetworkErrorLoggingServiceImpl::OnPoliciesLoaded,
+                       weak_factory_.GetWeakPtr()));
+  }
+
+  // This is called when loading from the store is complete, regardless of
+  // success or failure.
+  // DB initialization may have failed, in which case we will receive an empty
+  // vector from the PersistentNELStore. This is indistinguishable from a
+  // successful load that happens to not yield any policies, but in
+  // either case we still want to go through the task backlog.
+  void OnPoliciesLoaded(std::vector<NELPolicy> loaded_policies) {
+    DCHECK(PoliciesArePersisted());
+    DCHECK(!initialized_);
+
+    // TODO(chlily): Toss any expired policies we encounter.
+    for (NELPolicy& policy : loaded_policies) {
+      AddPolicy(std::move(policy));
+    }
+    initialized_ = true;
+    ExecuteBacklog();
+  }
 };
 
 }  // namespace
@@ -815,12 +987,13 @@
 
 void NetworkErrorLoggingService::SetReportingService(
     ReportingService* reporting_service) {
+  DCHECK(!reporting_service_);
   reporting_service_ = reporting_service;
 }
 
 void NetworkErrorLoggingService::OnShutdown() {
   shut_down_ = true;
-  SetReportingService(nullptr);
+  reporting_service_ = nullptr;
 }
 
 void NetworkErrorLoggingService::SetClockForTesting(const base::Clock* clock) {
diff --git a/net/network_error_logging/network_error_logging_service.h b/net/network_error_logging/network_error_logging_service.h
index 642e2fc..98e08ce1 100644
--- a/net/network_error_logging/network_error_logging_service.h
+++ b/net/network_error_logging/network_error_logging_service.h
@@ -227,7 +227,7 @@
 
   // Queues a Signed Exchange report.
   virtual void QueueSignedExchangeReport(
-      const SignedExchangeReportDetails& details) = 0;
+      SignedExchangeReportDetails details) = 0;
 
   // Removes browsing data (origin policies) associated with any origin for
   // which |origin_filter| returns true.
@@ -241,10 +241,12 @@
   // Sets the ReportingService that will be used to queue network error reports.
   // If |nullptr| is passed, reports will be queued locally or discarded.
   // |reporting_service| must outlive the NetworkErrorLoggingService.
+  // Should not be called again if previously called with a non-null pointer.
   void SetReportingService(ReportingService* reporting_service);
 
-  // Shuts down the NEL service so that no more requests or headers are
-  // processed and no more reports are queued.
+  // Shuts down the NEL service, so that no more requests or headers can be
+  // processed, no more reports are queued, and browsing data can no longer be
+  // cleared.
   void OnShutdown();
 
   // Sets a base::Clock (used to track policy expiration) for tests.
diff --git a/net/network_error_logging/network_error_logging_service_unittest.cc b/net/network_error_logging/network_error_logging_service_unittest.cc
index 8e274fd53..c2fefd5 100644
--- a/net/network_error_logging/network_error_logging_service_unittest.cc
+++ b/net/network_error_logging/network_error_logging_service_unittest.cc
@@ -18,8 +18,7 @@
 #include "net/base/net_errors.h"
 #include "net/network_error_logging/mock_persistent_nel_store.h"
 #include "net/network_error_logging/network_error_logging_service.h"
-#include "net/reporting/reporting_policy.h"
-#include "net/reporting/reporting_service.h"
+#include "net/reporting/reporting_test_util.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 #include "url/origin.h"
@@ -27,97 +26,15 @@
 namespace net {
 namespace {
 
-class TestReportingService : public ReportingService {
- public:
-  struct Report {
-    Report() = default;
-
-    Report(Report&& other)
-        : url(other.url),
-          user_agent(other.user_agent),
-          group(other.group),
-          type(other.type),
-          body(std::move(other.body)),
-          depth(other.depth) {}
-
-    Report(const GURL& url,
-           const std::string& user_agent,
-           const std::string& group,
-           const std::string& type,
-           std::unique_ptr<const base::Value> body,
-           int depth)
-        : url(url),
-          user_agent(user_agent),
-          group(group),
-          type(type),
-          body(std::move(body)),
-          depth(depth) {}
-
-    ~Report() = default;
-
-    GURL url;
-    std::string user_agent;
-    std::string group;
-    std::string type;
-    std::unique_ptr<const base::Value> body;
-    int depth;
-
-   private:
-    DISALLOW_COPY(Report);
-  };
-
-  TestReportingService() = default;
-
-  const std::vector<Report>& reports() const { return reports_; }
-
-  // ReportingService implementation:
-
-  ~TestReportingService() override = default;
-
-  void QueueReport(const GURL& url,
-                   const std::string& user_agent,
-                   const std::string& group,
-                   const std::string& type,
-                   std::unique_ptr<const base::Value> body,
-                   int depth) override {
-    reports_.push_back(
-        Report(url, user_agent, group, type, std::move(body), depth));
-  }
-
-  void ProcessHeader(const GURL& url,
-                     const std::string& header_value) override {
-    NOTREACHED();
-  }
-
-  void RemoveBrowsingData(int data_type_mask,
-                          const base::RepeatingCallback<bool(const GURL&)>&
-                              origin_filter) override {
-    NOTREACHED();
-  }
-
-  void RemoveAllBrowsingData(int data_type_mask) override { NOTREACHED(); }
-
-  void OnShutdown() override {}
-
-  const ReportingPolicy& GetPolicy() const override {
-    NOTREACHED();
-    return dummy_policy_;
-  }
-
-  ReportingContext* GetContextForTesting() const override {
-    NOTREACHED();
-    return nullptr;
-  }
-
- private:
-  std::vector<Report> reports_;
-  ReportingPolicy dummy_policy_;
-
-  DISALLOW_COPY_AND_ASSIGN(TestReportingService);
-};
-
 // The tests are parametrized on a boolean value which represents whether or not
 // to use a MockPersistentNELStore.
+// If a MockPersistentNELStore is used, then calls to
+// NetworkErrorLoggingService::OnHeader(), OnRequest(),
+// QueueSignedExchangeReport(), RemoveBrowsingData(), and
+// RemoveAllBrowsingData() will block until the store finishes loading.
+// Therefore, for tests that should run synchronously (i.e. tests that don't
+// specifically test the asynchronous/deferred task behavior), FinishLoading()
+// must be called after the first call to one of the above methods.
 class NetworkErrorLoggingServiceTest : public ::testing::TestWithParam<bool> {
  protected:
   NetworkErrorLoggingServiceTest() {
@@ -137,13 +54,6 @@
     service_->SetReportingService(reporting_service_.get());
   }
 
-  void DestroyReportingService() {
-    DCHECK(reporting_service_);
-
-    service_->SetReportingService(nullptr);
-    reporting_service_.reset();
-  }
-
   NetworkErrorLoggingService::RequestDetails MakeRequestDetails(
       GURL url,
       Error error_type,
@@ -198,6 +108,18 @@
     return url::Origin::Create(url);
   }
 
+  NetworkErrorLoggingService::NELPolicy MakePolicyForOrigin(
+      url::Origin origin,
+      base::Time expires = base::Time(),
+      base::Time last_used = base::Time()) {
+    NetworkErrorLoggingService::NELPolicy policy;
+    policy.origin = std::move(origin);
+    policy.expires = expires;
+    policy.last_used = last_used;
+
+    return policy;
+  }
+
   // Returns whether the NetworkErrorLoggingService has a policy corresponding
   // to |origin|. Returns true if so, even if the policy is expired.
   bool HasPolicyForOrigin(const url::Origin& origin) {
@@ -208,6 +130,12 @@
 
   size_t PolicyCount() { return service_->GetPolicyOriginsForTesting().size(); }
 
+  // Makes the rest of the test run synchronously.
+  void FinishLoading(bool load_success) {
+    if (store())
+      store()->FinishLoading(load_success);
+  }
+
   const GURL kUrl_ = GURL("https://example.com/path");
   const GURL kUrlDifferentPort_ = GURL("https://example.com:4433/path");
   const GURL kUrlSubdomain_ = GURL("https://subdomain.example.com/path");
@@ -247,7 +175,6 @@
 
   const GURL kReferrer_ = GURL("https://referrer.com/");
 
- private:
   // |store_| needs to outlive |service_|.
   std::unique_ptr<MockPersistentNELStore> store_;
   std::unique_ptr<NetworkErrorLoggingService> service_;
@@ -268,22 +195,32 @@
 }
 
 TEST_P(NetworkErrorLoggingServiceTest, NoReportingService) {
-  DestroyReportingService();
+  service_ = NetworkErrorLoggingService::Create(store_.get());
 
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
+  // Should not crash.
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED));
 }
 
 TEST_P(NetworkErrorLoggingServiceTest, NoPolicyForOrigin) {
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED));
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   EXPECT_TRUE(reports().empty());
 }
 
 TEST_P(NetworkErrorLoggingServiceTest, JsonTooLong) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderTooLong_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED));
 
   EXPECT_TRUE(reports().empty());
@@ -292,6 +229,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, JsonTooDeep) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderTooDeep_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED));
 
   EXPECT_TRUE(reports().empty());
@@ -300,6 +240,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, SuccessReportQueued) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, OK));
 
   ASSERT_EQ(1u, reports().size());
@@ -337,6 +280,9 @@
       "{\"report_to\":\"group\",\"max_age\":86400,\"failure_fraction\":1.0}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderFailureFraction1);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED));
 
   ASSERT_EQ(1u, reports().size());
@@ -374,6 +320,9 @@
       "{\"report_to\":\"group\",\"max_age\":86400,\"failure_fraction\":1.0}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderFailureFraction1);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   // This error code happens to not be mapped to a NEL report `type` field
   // value.
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_FILE_NO_SPACE));
@@ -392,6 +341,9 @@
       "{\"report_to\":\"group\",\"max_age\":86400,\"failure_fraction\":1.0}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderFailureFraction1);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   // This error code happens to not be mapped to a NEL report `type` field
   // value.  Because it's a certificate error, we'll set the `phase` to be
   // `connection`.
@@ -411,6 +363,9 @@
       "{\"report_to\":\"group\",\"max_age\":86400,\"failure_fraction\":1.0}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderFailureFraction1);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, OK, "GET", 504));
 
   ASSERT_EQ(1u, reports().size());
@@ -446,6 +401,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, SuccessReportDowngraded) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrl_, OK, "GET", 200, kOtherServerIP_));
 
@@ -480,6 +438,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, FailureReportDowngraded) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED, "GET",
                                           200, kOtherServerIP_));
 
@@ -514,6 +475,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, HttpErrorReportDowngraded) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrl_, OK, "GET", 504, kOtherServerIP_));
 
@@ -548,6 +512,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, DNSFailureReportNotDowngraded) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_NAME_NOT_RESOLVED, "GET",
                                           0, kOtherServerIP_));
 
@@ -582,6 +549,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, SuccessPOSTReportQueued) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, OK, "POST"));
 
   ASSERT_EQ(1u, reports().size());
@@ -610,6 +580,10 @@
 
 TEST_P(NetworkErrorLoggingServiceTest, MaxAge0) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   EXPECT_EQ(1u, PolicyCount());
 
   // Max_age of 0 removes the policy.
@@ -624,6 +598,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, SuccessFraction0) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction0_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   // Each network error has a 0% chance of being reported.  Fire off several and
   // verify that no reports are produced.
   constexpr size_t kReportCount = 100;
@@ -641,6 +618,9 @@
       "\"failure_fraction\":0.25}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFractionHalf);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   // Each network error has a 50% chance of being reported.  Fire off several
   // and verify that some requests were reported and some weren't.  (We can't
   // verify exact counts because each decision is made randomly.)
@@ -672,6 +652,9 @@
       "{\"report_to\":\"group\",\"max_age\":86400,\"failure_fraction\":0.0}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderFailureFraction0);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   // Each network error has a 0% chance of being reported.  Fire off several and
   // verify that no reports are produced.
   constexpr size_t kReportCount = 100;
@@ -689,6 +672,9 @@
       "\"success_fraction\":0.25}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderFailureFractionHalf);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   // Each network error has a 50% chance of being reported.  Fire off several
   // and verify that some requests were reported and some weren't.  (We can't
   // verify exact counts because each decision is made randomly.)
@@ -717,6 +703,9 @@
        ExcludeSubdomainsDoesntMatchDifferentPort) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrlDifferentPort_, ERR_CONNECTION_REFUSED));
 
@@ -726,6 +715,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, ExcludeSubdomainsDoesntMatchSubdomain) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrlSubdomain_, ERR_CONNECTION_REFUSED));
 
@@ -735,6 +727,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, IncludeSubdomainsMatchesDifferentPort) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrlDifferentPort_, ERR_NAME_NOT_RESOLVED));
 
@@ -745,6 +740,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, IncludeSubdomainsMatchesSubdomain) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrlSubdomain_, ERR_NAME_NOT_RESOLVED));
 
@@ -755,6 +753,9 @@
        IncludeSubdomainsDoesntMatchSuperdomain) {
   service()->OnHeader(kOriginSubdomain_, kServerIP_, kHeaderIncludeSubdomains_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, ERR_NAME_NOT_RESOLVED));
 
   EXPECT_TRUE(reports().empty());
@@ -764,6 +765,9 @@
        IncludeSubdomainsDoesntReportConnectionError) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrlSubdomain_, ERR_CONNECTION_REFUSED));
 
@@ -774,6 +778,9 @@
        IncludeSubdomainsDoesntReportApplicationError) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(
       MakeRequestDetails(kUrlSubdomain_, ERR_INVALID_HTTP_RESPONSE));
 
@@ -783,6 +790,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, IncludeSubdomainsDoesntReportSuccess) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrlSubdomain_, OK));
 
   EXPECT_TRUE(reports().empty());
@@ -795,6 +805,9 @@
       "\"include_subdomains\":true,\"success_fraction\":1.0}";
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderIncludeSubdomainsSuccess1);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnRequest(MakeRequestDetails(kUrl_, OK));
 
   ASSERT_EQ(1u, reports().size());
@@ -803,6 +816,10 @@
 
 TEST_P(NetworkErrorLoggingServiceTest, RemoveAllBrowsingData) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   EXPECT_EQ(1u, PolicyCount());
   EXPECT_TRUE(HasPolicyForOrigin(kOrigin_));
 
@@ -817,6 +834,10 @@
 
 TEST_P(NetworkErrorLoggingServiceTest, RemoveSomeBrowsingData) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnHeader(kOriginDifferentHost_, kServerIP_, kHeader_);
   EXPECT_EQ(2u, PolicyCount());
 
@@ -842,6 +863,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, Nested) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   NetworkErrorLoggingService::RequestDetails details =
       MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED);
   details.reporting_upload_depth =
@@ -856,6 +880,9 @@
 TEST_P(NetworkErrorLoggingServiceTest, NestedTooDeep) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   NetworkErrorLoggingService::RequestDetails details =
       MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED);
   details.reporting_upload_depth =
@@ -880,6 +907,10 @@
   clock.Advance(delta_from_origin);
 
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->OnHeader(kOriginDifferentHost_, kServerIP_, kHeader_);
   service()->OnHeader(kOriginSubdomain_, kServerIP_, kHeaderIncludeSubdomains_);
   const std::string kHeaderWrongTypes =
@@ -938,9 +969,14 @@
 }
 
 TEST_P(NetworkErrorLoggingServiceTest, NoReportingService_SignedExchange) {
-  DestroyReportingService();
+  service_ = NetworkErrorLoggingService::Create(store_.get());
 
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
+  // Should not crash
   service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
       false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
 }
@@ -948,12 +984,19 @@
 TEST_P(NetworkErrorLoggingServiceTest, NoPolicyForOrigin_SignedExchange) {
   service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
       false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   EXPECT_TRUE(reports().empty());
 }
 
 TEST_P(NetworkErrorLoggingServiceTest, SuccessFraction0_SignedExchange) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction0_);
 
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   // Each network error has a 0% chance of being reported.  Fire off several and
   // verify that no reports are produced.
   constexpr size_t kReportCount = 100;
@@ -967,6 +1010,10 @@
 
 TEST_P(NetworkErrorLoggingServiceTest, SuccessReportQueued_SignedExchange) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderSuccessFraction1_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
       true, "ok", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
   ASSERT_EQ(1u, reports().size());
@@ -1013,6 +1060,10 @@
 
 TEST_P(NetworkErrorLoggingServiceTest, FailureReportQueued_SignedExchange) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
       false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
   ASSERT_EQ(1u, reports().size());
@@ -1059,6 +1110,10 @@
 
 TEST_P(NetworkErrorLoggingServiceTest, MismatchingSubdomain_SignedExchange) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeaderIncludeSubdomains_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
       false, "sxg.failed", kUrlSubdomain_, kInnerUrl_, kCertUrl_, kServerIP_));
   EXPECT_TRUE(reports().empty());
@@ -1066,6 +1121,10 @@
 
 TEST_P(NetworkErrorLoggingServiceTest, MismatchingIPAddress_SignedExchange) {
   service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
       false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kOtherServerIP_));
   EXPECT_TRUE(reports().empty());
@@ -1081,6 +1140,9 @@
   for (size_t i = 0; i < 100; ++i) {
     service()->OnHeader(MakeOrigin(i), kServerIP_, kHeader_);
   }
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+
   EXPECT_EQ(100u, PolicyCount());
   clock.Advance(base::TimeDelta::FromSeconds(86401));  // max_age is 86400 sec
   // Expired policies are allowed to linger before hitting the policy limit.
@@ -1106,6 +1168,8 @@
     service()->OnHeader(MakeOrigin(i), kServerIP_, kHeader_);
     clock.Advance(base::TimeDelta::FromSeconds(1));
   }
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
 
   EXPECT_EQ(PolicyCount(), NetworkErrorLoggingService::kMaxPolicies);
 
@@ -1152,6 +1216,315 @@
   // this test.
 }
 
+TEST_P(NetworkErrorLoggingServiceTest, SendsCommandsToStoreSynchronous) {
+  if (!store())
+    return;
+
+  MockPersistentNELStore::CommandList expected_commands;
+  NetworkErrorLoggingService::NELPolicy policy1 = MakePolicyForOrigin(kOrigin_);
+  NetworkErrorLoggingService::NELPolicy policy2 =
+      MakePolicyForOrigin(kOriginDifferentHost_);
+  std::vector<NetworkErrorLoggingService::NELPolicy> prestored_policies = {
+      policy1, policy2};
+  store()->SetPrestoredPolicies(std::move(prestored_policies));
+
+  // The first call to any of the public methods triggers a load.
+  service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::LOAD_NEL_POLICIES);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(true /* load_success */);
+  // DoOnHeader() should now execute.
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy1);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::ADD_NEL_POLICY, policy1);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->OnRequest(
+      MakeRequestDetails(kOrigin_.GetURL(), ERR_CONNECTION_REFUSED));
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
+      false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  // Removes policy1 but not policy2.
+  EXPECT_EQ(2, store()->StoredPoliciesCount());
+  service()->RemoveBrowsingData(
+      base::BindRepeating([](const GURL& origin) -> bool {
+        return origin.host() == "example.com";
+      }));
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy1);
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  EXPECT_EQ(1, store()->StoredPoliciesCount());
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->RemoveAllBrowsingData();
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy2);
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  EXPECT_EQ(0, store()->StoredPoliciesCount());
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+}
+
+// Same as the above test, except that all the tasks are queued until loading
+// is complete.
+TEST_P(NetworkErrorLoggingServiceTest, SendsCommandsToStoreDeferred) {
+  if (!store())
+    return;
+
+  MockPersistentNELStore::CommandList expected_commands;
+  NetworkErrorLoggingService::NELPolicy policy1 = MakePolicyForOrigin(kOrigin_);
+  NetworkErrorLoggingService::NELPolicy policy2 =
+      MakePolicyForOrigin(kOriginDifferentHost_);
+  std::vector<NetworkErrorLoggingService::NELPolicy> prestored_policies = {
+      policy1, policy2};
+  store()->SetPrestoredPolicies(std::move(prestored_policies));
+
+  // The first call to any of the public methods triggers a load.
+  service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::LOAD_NEL_POLICIES);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->OnRequest(
+      MakeRequestDetails(kOrigin_.GetURL(), ERR_CONNECTION_REFUSED));
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
+      false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  // Removes policy1 but not policy2.
+  service()->RemoveBrowsingData(
+      base::BindRepeating([](const GURL& origin) -> bool {
+        return origin.host() == "example.com";
+      }));
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->RemoveAllBrowsingData();
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  // The store has not yet been told to remove the policies because the tasks
+  // to remove browsing data were queued pending initialization.
+  EXPECT_EQ(2, store()->StoredPoliciesCount());
+
+  FinishLoading(true /* load_success */);
+  // DoOnHeader()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy1);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::ADD_NEL_POLICY, policy1);
+  // DoOnRequest()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  // DoQueueSignedExchangeReport()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  // DoRemoveBrowsingData()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy1);
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  // DoRemoveAllBrowsingData()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy2);
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+}
+
+// These two tests check that if loading fails, the commands should still
+// be sent to the store; the actual store impl will just ignore them.
+TEST_P(NetworkErrorLoggingServiceTest,
+       SendsCommandsToStoreSynchronousLoadFailed) {
+  if (!store())
+    return;
+
+  MockPersistentNELStore::CommandList expected_commands;
+  NetworkErrorLoggingService::NELPolicy policy1 = MakePolicyForOrigin(kOrigin_);
+  NetworkErrorLoggingService::NELPolicy policy2 =
+      MakePolicyForOrigin(kOriginDifferentHost_);
+  std::vector<NetworkErrorLoggingService::NELPolicy> prestored_policies = {
+      policy1, policy2};
+  store()->SetPrestoredPolicies(std::move(prestored_policies));
+
+  // The first call to any of the public methods triggers a load.
+  service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::LOAD_NEL_POLICIES);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  // Make the rest of the test run synchronously.
+  FinishLoading(false /* load_success */);
+  // DoOnHeader() should now execute.
+  // Because the load failed, there will be no policies in memory, so the store
+  // is not told to delete anything.
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::ADD_NEL_POLICY, policy1);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+  LOG(INFO) << store()->GetDebugString();
+
+  service()->OnRequest(
+      MakeRequestDetails(kOrigin_.GetURL(), ERR_CONNECTION_REFUSED));
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
+      false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  // Removes policy1 but not policy2.
+  service()->RemoveBrowsingData(
+      base::BindRepeating([](const GURL& origin) -> bool {
+        return origin.host() == "example.com";
+      }));
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy1);
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->RemoveAllBrowsingData();
+  // We failed to load policy2 from the store, so there is nothing to remove
+  // here.
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+}
+
+TEST_P(NetworkErrorLoggingServiceTest, SendsCommandsToStoreDeferredLoadFailed) {
+  if (!store())
+    return;
+
+  MockPersistentNELStore::CommandList expected_commands;
+  NetworkErrorLoggingService::NELPolicy policy1 = MakePolicyForOrigin(kOrigin_);
+  NetworkErrorLoggingService::NELPolicy policy2 =
+      MakePolicyForOrigin(kOriginDifferentHost_);
+  std::vector<NetworkErrorLoggingService::NELPolicy> prestored_policies = {
+      policy1, policy2};
+  store()->SetPrestoredPolicies(std::move(prestored_policies));
+
+  // The first call to any of the public methods triggers a load.
+  service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::LOAD_NEL_POLICIES);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->OnRequest(
+      MakeRequestDetails(kOrigin_.GetURL(), ERR_CONNECTION_REFUSED));
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
+      false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  // Removes policy1 but not policy2.
+  service()->RemoveBrowsingData(
+      base::BindRepeating([](const GURL& origin) -> bool {
+        return origin.host() == "example.com";
+      }));
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->RemoveAllBrowsingData();
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  FinishLoading(false /* load_success */);
+  // DoOnHeader()
+  // Because the load failed, there will be no policies in memory, so the store
+  // is not told to delete anything.
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::ADD_NEL_POLICY, policy1);
+  // DoOnRequest()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  // DoQueueSignedExchangeReport()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::UPDATE_NEL_POLICY, policy1);
+  // DoRemoveBrowsingData()
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::DELETE_NEL_POLICY, policy1);
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  // DoRemoveAllBrowsingData()
+  // We failed to load policy2 from the store, so there is nothing to remove
+  // here.
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+}
+
+TEST_P(NetworkErrorLoggingServiceTest, FlushesStoreOnDestruction) {
+  auto store = std::make_unique<MockPersistentNELStore>();
+  std::unique_ptr<NetworkErrorLoggingService> service =
+      NetworkErrorLoggingService::Create(store.get());
+
+  MockPersistentNELStore::CommandList expected_commands;
+
+  service->OnHeader(kOrigin_, kServerIP_, kHeader_);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::LOAD_NEL_POLICIES);
+  EXPECT_TRUE(store->VerifyCommands(expected_commands));
+
+  store->FinishLoading(false /* load_success */);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::ADD_NEL_POLICY,
+      MakePolicyForOrigin(kOrigin_));
+  EXPECT_TRUE(store->VerifyCommands(expected_commands));
+
+  // Store should be flushed on destruction of service.
+  service.reset();
+  expected_commands.emplace_back(MockPersistentNELStore::Command::Type::FLUSH);
+  EXPECT_TRUE(store->VerifyCommands(expected_commands));
+}
+
+TEST_P(NetworkErrorLoggingServiceTest,
+       DoesntFlushStoreOnDestructionBeforeLoad) {
+  auto store = std::make_unique<MockPersistentNELStore>();
+  std::unique_ptr<NetworkErrorLoggingService> service =
+      NetworkErrorLoggingService::Create(store.get());
+
+  service.reset();
+  EXPECT_EQ(0u, store->GetAllCommands().size());
+}
+
+TEST_P(NetworkErrorLoggingServiceTest, DoNothingIfShutDown) {
+  if (!store())
+    return;
+
+  MockPersistentNELStore::CommandList expected_commands;
+
+  // The first call to any of the public methods triggers a load.
+  service()->OnHeader(kOrigin_, kServerIP_, kHeader_);
+  expected_commands.emplace_back(
+      MockPersistentNELStore::Command::Type::LOAD_NEL_POLICIES);
+  EXPECT_TRUE(store()->VerifyCommands(expected_commands));
+
+  service()->OnRequest(
+      MakeRequestDetails(kOrigin_.GetURL(), ERR_CONNECTION_REFUSED));
+  service()->QueueSignedExchangeReport(MakeSignedExchangeReportDetails(
+      false, "sxg.failed", kUrl_, kInnerUrl_, kCertUrl_, kServerIP_));
+  service()->RemoveBrowsingData(
+      base::BindRepeating([](const GURL& origin) -> bool {
+        return origin.host() == "example.com";
+      }));
+  service()->RemoveAllBrowsingData();
+
+  // Finish loading after the service has been shut down.
+  service()->OnShutdown();
+  FinishLoading(true /* load_success */);
+
+  // Only the LOAD command should have been sent to the store.
+  EXPECT_EQ(1u, store()->GetAllCommands().size());
+  EXPECT_EQ(0u, PolicyCount());
+  EXPECT_EQ(0u, reports().size());
+}
+
 INSTANTIATE_TEST_SUITE_P(NetworkErrorLoggingServiceStoreTest,
                          NetworkErrorLoggingServiceTest,
                          testing::Bool());
diff --git a/net/network_error_logging/network_error_logging_test_util.cc b/net/network_error_logging/network_error_logging_test_util.cc
index 54d6376..a75a06d8 100644
--- a/net/network_error_logging/network_error_logging_test_util.cc
+++ b/net/network_error_logging/network_error_logging_test_util.cc
@@ -33,7 +33,7 @@
 }
 
 void TestNetworkErrorLoggingService::QueueSignedExchangeReport(
-    const SignedExchangeReportDetails& details) {}
+    SignedExchangeReportDetails details) {}
 
 void TestNetworkErrorLoggingService::RemoveBrowsingData(
     const base::RepeatingCallback<bool(const GURL&)>& origin_filter) {}
diff --git a/net/network_error_logging/network_error_logging_test_util.h b/net/network_error_logging/network_error_logging_test_util.h
index 3eb29c3..c6af941 100644
--- a/net/network_error_logging/network_error_logging_test_util.h
+++ b/net/network_error_logging/network_error_logging_test_util.h
@@ -48,8 +48,7 @@
                 const IPAddress& received_ip_address,
                 const std::string& value) override;
   void OnRequest(RequestDetails details) override;
-  void QueueSignedExchangeReport(
-      const SignedExchangeReportDetails& details) override;
+  void QueueSignedExchangeReport(SignedExchangeReportDetails details) override;
   void RemoveBrowsingData(
       const base::RepeatingCallback<bool(const GURL&)>& origin_filter) override;
   void RemoveAllBrowsingData() override;
diff --git a/net/reporting/reporting_test_util.cc b/net/reporting/reporting_test_util.cc
index c78789f..c9383f9a 100644
--- a/net/reporting/reporting_test_util.cc
+++ b/net/reporting/reporting_test_util.cc
@@ -283,4 +283,70 @@
   return tick_clock()->NowTicks() + base::TimeDelta::FromDays(1);
 }
 
+TestReportingService::Report::Report() = default;
+
+TestReportingService::Report::Report(Report&& other)
+    : url(other.url),
+      user_agent(other.user_agent),
+      group(other.group),
+      type(other.type),
+      body(std::move(other.body)),
+      depth(other.depth) {}
+
+TestReportingService::Report::Report(const GURL& url,
+                                     const std::string& user_agent,
+                                     const std::string& group,
+                                     const std::string& type,
+                                     std::unique_ptr<const base::Value> body,
+                                     int depth)
+    : url(url),
+      user_agent(user_agent),
+      group(group),
+      type(type),
+      body(std::move(body)),
+      depth(depth) {}
+
+TestReportingService::Report::~Report() = default;
+
+TestReportingService::TestReportingService() = default;
+
+TestReportingService::~TestReportingService() = default;
+
+void TestReportingService::QueueReport(const GURL& url,
+                                       const std::string& user_agent,
+                                       const std::string& group,
+                                       const std::string& type,
+                                       std::unique_ptr<const base::Value> body,
+                                       int depth) {
+  reports_.push_back(
+      Report(url, user_agent, group, type, std::move(body), depth));
+}
+
+void TestReportingService::ProcessHeader(const GURL& url,
+                                         const std::string& header_value) {
+  NOTREACHED();
+}
+
+void TestReportingService::RemoveBrowsingData(
+    int data_type_mask,
+    const base::RepeatingCallback<bool(const GURL&)>& origin_filter) {
+  NOTREACHED();
+}
+
+void TestReportingService::RemoveAllBrowsingData(int data_type_mask) {
+  NOTREACHED();
+}
+
+void TestReportingService::OnShutdown() {}
+
+const ReportingPolicy& TestReportingService::GetPolicy() const {
+  NOTREACHED();
+  return dummy_policy_;
+}
+
+ReportingContext* TestReportingService::GetContextForTesting() const {
+  NOTREACHED();
+  return nullptr;
+}
+
 }  // namespace net
diff --git a/net/reporting/reporting_test_util.h b/net/reporting/reporting_test_util.h
index b6a558c..f290c3de 100644
--- a/net/reporting/reporting_test_util.h
+++ b/net/reporting/reporting_test_util.h
@@ -16,8 +16,10 @@
 #include "net/reporting/reporting_cache.h"
 #include "net/reporting/reporting_context.h"
 #include "net/reporting/reporting_delegate.h"
+#include "net/reporting/reporting_service.h"
 #include "net/reporting/reporting_uploader.h"
 #include "net/test/test_with_scoped_task_environment.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
@@ -38,6 +40,15 @@
 class ReportingGarbageCollector;
 class TestURLRequestContext;
 
+// A matcher for ReportingReports, which checks that the url of the report is
+// the given url.
+// Usage: EXPECT_THAT(report, ReportUrlIs(url));
+// EXPECT_THAT(reports(),
+//             testing::ElementsAre(ReportUrlIs(url1), ReportUrlIs(url2)));
+MATCHER_P(ReportUrlIs, url, "") {
+  return arg.url == url;
+}
+
 // A test implementation of ReportingUploader that holds uploads for tests to
 // examine and complete with a specified outcome.
 class TestReportingUploader : public ReportingUploader {
@@ -267,6 +278,69 @@
   DISALLOW_COPY_AND_ASSIGN(ReportingTestBase);
 };
 
+class TestReportingService : public ReportingService {
+ public:
+  struct Report {
+    Report();
+
+    Report(Report&& other);
+
+    Report(const GURL& url,
+           const std::string& user_agent,
+           const std::string& group,
+           const std::string& type,
+           std::unique_ptr<const base::Value> body,
+           int depth);
+
+    ~Report();
+
+    GURL url;
+    std::string user_agent;
+    std::string group;
+    std::string type;
+    std::unique_ptr<const base::Value> body;
+    int depth;
+
+   private:
+    DISALLOW_COPY(Report);
+  };
+
+  TestReportingService();
+
+  const std::vector<Report>& reports() const { return reports_; }
+
+  // ReportingService implementation:
+
+  ~TestReportingService() override;
+
+  void QueueReport(const GURL& url,
+                   const std::string& user_agent,
+                   const std::string& group,
+                   const std::string& type,
+                   std::unique_ptr<const base::Value> body,
+                   int depth) override;
+
+  void ProcessHeader(const GURL& url, const std::string& header_value) override;
+
+  void RemoveBrowsingData(
+      int data_type_mask,
+      const base::RepeatingCallback<bool(const GURL&)>& origin_filter) override;
+
+  void RemoveAllBrowsingData(int data_type_mask) override;
+
+  void OnShutdown() override;
+
+  const ReportingPolicy& GetPolicy() const override;
+
+  ReportingContext* GetContextForTesting() const override;
+
+ private:
+  std::vector<Report> reports_;
+  ReportingPolicy dummy_policy_;
+
+  DISALLOW_COPY_AND_ASSIGN(TestReportingService);
+};
+
 }  // namespace net
 
 #endif  // NET_REPORTING_REPORTING_TEST_UTIL_H_
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index 545324be..ba7bc6c7 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -932,7 +932,7 @@
   details.status_code = report->status_code;
   details.elapsed_time = report->elapsed_time;
   details.user_agent = std::move(user_agent);
-  logging_service->QueueSignedExchangeReport(details);
+  logging_service->QueueSignedExchangeReport(std::move(details));
 }
 
 #else   // BUILDFLAG(ENABLE_REPORTING)