blob: f2d3426438dcfe4151c1d5363b35b676916ad15a [file] [log] [blame]
// Copyright 2013 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 "content/browser/service_worker/service_worker_storage.h"
#include <stdint.h>
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "content/browser/service_worker/embedded_worker_test_helper.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_disk_cache.h"
#include "content/browser/service_worker/service_worker_registration.h"
#include "content/browser/service_worker/service_worker_test_utils.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/common/service_worker/service_worker_utils.h"
#include "content/public/common/content_client.h"
#include "content/public/common/origin_util.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_utils.h"
#include "ipc/ipc_message.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/test_completion_callback.h"
#include "net/disk_cache/disk_cache.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/origin_trials/origin_trial_policy.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_object.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"
using net::IOBuffer;
using net::TestCompletionCallback;
using net::WrappedIOBuffer;
namespace content {
namespace service_worker_storage_unittest {
using RegistrationData = ServiceWorkerDatabase::RegistrationData;
using ResourceRecord = ServiceWorkerDatabase::ResourceRecord;
// This is a sample public key for testing the API. The corresponding private
// key (use this to generate new samples for this test file) is:
//
// 0x83, 0x67, 0xf4, 0xcd, 0x2a, 0x1f, 0x0e, 0x04, 0x0d, 0x43, 0x13,
// 0x4c, 0x67, 0xc4, 0xf4, 0x28, 0xc9, 0x90, 0x15, 0x02, 0xe2, 0xba,
// 0xfd, 0xbb, 0xfa, 0xbc, 0x92, 0x76, 0x8a, 0x2c, 0x4b, 0xc7, 0x75,
// 0x10, 0xac, 0xf9, 0x3a, 0x1c, 0xb8, 0xa9, 0x28, 0x70, 0xd2, 0x9a,
// 0xd0, 0x0b, 0x59, 0xe1, 0xac, 0x2b, 0xb7, 0xd5, 0xca, 0x1f, 0x64,
// 0x90, 0x08, 0x8e, 0xa8, 0xe0, 0x56, 0x3a, 0x04, 0xd0
const uint8_t kTestPublicKey[] = {
0x75, 0x10, 0xac, 0xf9, 0x3a, 0x1c, 0xb8, 0xa9, 0x28, 0x70, 0xd2,
0x9a, 0xd0, 0x0b, 0x59, 0xe1, 0xac, 0x2b, 0xb7, 0xd5, 0xca, 0x1f,
0x64, 0x90, 0x08, 0x8e, 0xa8, 0xe0, 0x56, 0x3a, 0x04, 0xd0,
};
void StatusAndQuitCallback(blink::ServiceWorkerStatusCode* result,
base::OnceClosure quit_closure,
blink::ServiceWorkerStatusCode status) {
*result = status;
std::move(quit_closure).Run();
}
void StatusCallback(bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* result,
blink::ServiceWorkerStatusCode status) {
*was_called = true;
*result = status;
}
ServiceWorkerStorage::StatusCallback MakeStatusCallback(
bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* result) {
return base::BindOnce(&StatusCallback, was_called, result);
}
void FindCallback(bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* result,
scoped_refptr<ServiceWorkerRegistration>* found,
blink::ServiceWorkerStatusCode status,
scoped_refptr<ServiceWorkerRegistration> registration) {
*was_called = true;
*result = status;
*found = std::move(registration);
}
ServiceWorkerStorage::FindRegistrationCallback MakeFindCallback(
bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* result,
scoped_refptr<ServiceWorkerRegistration>* found) {
return base::BindOnce(&FindCallback, was_called, result, found);
}
void GetAllCallback(
bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* result,
std::vector<scoped_refptr<ServiceWorkerRegistration>>* all_out,
blink::ServiceWorkerStatusCode status,
const std::vector<scoped_refptr<ServiceWorkerRegistration>>& all) {
*was_called = true;
*result = status;
*all_out = all;
}
void GetAllInfosCallback(
bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* result,
std::vector<ServiceWorkerRegistrationInfo>* all_out,
blink::ServiceWorkerStatusCode status,
const std::vector<ServiceWorkerRegistrationInfo>& all) {
*was_called = true;
*result = status;
*all_out = all;
}
ServiceWorkerStorage::GetRegistrationsCallback MakeGetRegistrationsCallback(
bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* status,
std::vector<scoped_refptr<ServiceWorkerRegistration>>* all) {
return base::BindOnce(&GetAllCallback, was_called, status, all);
}
ServiceWorkerStorage::GetRegistrationsInfosCallback
MakeGetRegistrationsInfosCallback(
bool* was_called,
base::Optional<blink::ServiceWorkerStatusCode>* status,
std::vector<ServiceWorkerRegistrationInfo>* all) {
return base::BindOnce(&GetAllInfosCallback, was_called, status, all);
}
void GetUserDataCallback(
bool* was_called,
std::vector<std::string>* data_out,
base::Optional<blink::ServiceWorkerStatusCode>* status_out,
const std::vector<std::string>& data,
blink::ServiceWorkerStatusCode status) {
*was_called = true;
*data_out = data;
*status_out = status;
}
void GetUserDataForAllRegistrationsCallback(
bool* was_called,
std::vector<std::pair<int64_t, std::string>>* data_out,
base::Optional<blink::ServiceWorkerStatusCode>* status_out,
const std::vector<std::pair<int64_t, std::string>>& data,
blink::ServiceWorkerStatusCode status) {
*was_called = true;
*data_out = data;
*status_out = status;
}
int WriteResponse(ServiceWorkerStorage* storage,
int64_t id,
const std::string& headers,
IOBuffer* body,
int length) {
std::unique_ptr<ServiceWorkerResponseWriter> writer =
storage->CreateResponseWriter(id);
std::unique_ptr<net::HttpResponseInfo> info =
std::make_unique<net::HttpResponseInfo>();
info->request_time = base::Time::Now();
info->response_time = base::Time::Now();
info->was_cached = false;
info->headers = new net::HttpResponseHeaders(headers);
scoped_refptr<HttpResponseInfoIOBuffer> info_buffer =
base::MakeRefCounted<HttpResponseInfoIOBuffer>(std::move(info));
int rv = 0;
{
TestCompletionCallback cb;
writer->WriteInfo(info_buffer.get(), cb.callback());
rv = cb.WaitForResult();
if (rv < 0)
return rv;
}
{
TestCompletionCallback cb;
writer->WriteData(body, length, cb.callback());
rv = cb.WaitForResult();
}
return rv;
}
int WriteStringResponse(ServiceWorkerStorage* storage,
int64_t id,
const std::string& headers,
const std::string& body) {
scoped_refptr<IOBuffer> body_buffer =
base::MakeRefCounted<WrappedIOBuffer>(body.data());
return WriteResponse(storage, id, headers, body_buffer.get(), body.length());
}
int WriteBasicResponse(ServiceWorkerStorage* storage, int64_t id) {
const char kHttpHeaders[] = "HTTP/1.0 200 HONKYDORY\0Content-Length: 5\0\0";
const char kHttpBody[] = "Hello";
std::string headers(kHttpHeaders, base::size(kHttpHeaders));
return WriteStringResponse(storage, id, headers, std::string(kHttpBody));
}
int ReadResponseInfo(ServiceWorkerStorage* storage,
int64_t id,
HttpResponseInfoIOBuffer* info_buffer) {
std::unique_ptr<ServiceWorkerResponseReader> reader =
storage->CreateResponseReader(id);
TestCompletionCallback cb;
reader->ReadInfo(info_buffer, cb.callback());
return cb.WaitForResult();
}
bool VerifyBasicResponse(ServiceWorkerStorage* storage,
int64_t id,
bool expected_positive_result) {
const std::string kExpectedHttpBody("Hello");
std::unique_ptr<ServiceWorkerResponseReader> reader =
storage->CreateResponseReader(id);
scoped_refptr<HttpResponseInfoIOBuffer> info_buffer =
new HttpResponseInfoIOBuffer();
int rv = ReadResponseInfo(storage, id, info_buffer.get());
if (expected_positive_result)
EXPECT_LT(0, rv);
if (rv <= 0)
return false;
std::string received_body;
const int kBigEnough = 512;
scoped_refptr<net::IOBuffer> buffer =
base::MakeRefCounted<IOBuffer>(kBigEnough);
TestCompletionCallback cb;
reader->ReadData(buffer.get(), kBigEnough, cb.callback());
rv = cb.WaitForResult();
EXPECT_EQ(static_cast<int>(kExpectedHttpBody.size()), rv);
if (rv <= 0)
return false;
received_body.assign(buffer->data(), rv);
bool status_match =
std::string("HONKYDORY") ==
info_buffer->http_info->headers->GetStatusText();
bool data_match = kExpectedHttpBody == received_body;
EXPECT_TRUE(status_match);
EXPECT_TRUE(data_match);
return status_match && data_match;
}
int WriteResponseMetadata(ServiceWorkerStorage* storage,
int64_t id,
const std::string& metadata) {
scoped_refptr<IOBuffer> body_buffer =
base::MakeRefCounted<WrappedIOBuffer>(metadata.data());
std::unique_ptr<ServiceWorkerResponseMetadataWriter> metadata_writer =
storage->CreateResponseMetadataWriter(id);
TestCompletionCallback cb;
metadata_writer->WriteMetadata(body_buffer.get(), metadata.length(),
cb.callback());
return cb.WaitForResult();
}
int WriteMetadata(ServiceWorkerVersion* version,
const GURL& url,
const std::string& metadata) {
const std::vector<uint8_t> data(metadata.begin(), metadata.end());
EXPECT_TRUE(version);
TestCompletionCallback cb;
version->script_cache_map()->WriteMetadata(url, data, cb.callback());
return cb.WaitForResult();
}
int ClearMetadata(ServiceWorkerVersion* version, const GURL& url) {
EXPECT_TRUE(version);
TestCompletionCallback cb;
version->script_cache_map()->ClearMetadata(url, cb.callback());
return cb.WaitForResult();
}
bool VerifyResponseMetadata(ServiceWorkerStorage* storage,
int64_t id,
const std::string& expected_metadata) {
std::unique_ptr<ServiceWorkerResponseReader> reader =
storage->CreateResponseReader(id);
scoped_refptr<HttpResponseInfoIOBuffer> info_buffer =
new HttpResponseInfoIOBuffer();
{
TestCompletionCallback cb;
reader->ReadInfo(info_buffer.get(), cb.callback());
int rv = cb.WaitForResult();
EXPECT_LT(0, rv);
}
const net::HttpResponseInfo* read_head = info_buffer->http_info.get();
if (!read_head->metadata.get())
return false;
EXPECT_EQ(0, memcmp(expected_metadata.data(), read_head->metadata->data(),
expected_metadata.length()));
return true;
}
class ServiceWorkerStorageTest : public testing::Test {
public:
ServiceWorkerStorageTest()
: browser_thread_bundle_(TestBrowserThreadBundle::IO_MAINLOOP) {
}
void SetUp() override { InitializeTestHelper(); }
void TearDown() override {
helper_.reset();
disk_cache::FlushCacheThreadForTesting();
content::RunAllTasksUntilIdle();
}
bool InitUserDataDirectory() {
if (!user_data_directory_.CreateUniqueTempDir())
return false;
user_data_directory_path_ = user_data_directory_.GetPath();
return true;
}
void InitializeTestHelper() {
helper_.reset(new EmbeddedWorkerTestHelper(user_data_directory_path_));
base::RunLoop().RunUntilIdle();
}
ServiceWorkerContextCore* context() { return helper_->context(); }
ServiceWorkerStorage* storage() { return context()->storage(); }
ServiceWorkerDatabase* database() { return storage()->database_.get(); }
// A static class method for friendliness.
static void VerifyPurgeableListStatusCallback(
ServiceWorkerDatabase* database,
std::set<int64_t>* purgeable_ids,
bool* was_called,
blink::ServiceWorkerStatusCode* result,
blink::ServiceWorkerStatusCode status) {
*was_called = true;
*result = status;
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database->GetPurgeableResourceIds(purgeable_ids));
}
protected:
const std::set<GURL>& registered_origins() {
return storage()->registered_origins_;
}
void LazyInitialize() {
storage()->LazyInitializeForTest(base::DoNothing());
base::RunLoop().RunUntilIdle();
}
// Creates a registration with a waiting version in INSTALLED state.
scoped_refptr<ServiceWorkerRegistration> CreateLiveRegistrationAndVersion(
const GURL& scope,
const GURL& script) {
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = scope;
auto registration = base::MakeRefCounted<ServiceWorkerRegistration>(
options, storage()->NewRegistrationId(), context()->AsWeakPtr());
auto version = base::MakeRefCounted<ServiceWorkerVersion>(
registration.get(), script, blink::mojom::ScriptType::kClassic,
storage()->NewVersionId(), context()->AsWeakPtr());
std::vector<ResourceRecord> records = {
ResourceRecord(storage()->NewResourceId(), script, 100)};
version->script_cache_map()->SetResources(records);
version->set_fetch_handler_existence(
ServiceWorkerVersion::FetchHandlerExistence::EXISTS);
version->SetStatus(ServiceWorkerVersion::INSTALLED);
registration->SetWaitingVersion(version);
return registration;
}
blink::ServiceWorkerStatusCode StoreRegistration(
scoped_refptr<ServiceWorkerRegistration> registration,
scoped_refptr<ServiceWorkerVersion> version) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->StoreRegistration(registration.get(),
version.get(),
MakeStatusCallback(&was_called, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode DeleteRegistration(int64_t registration_id,
const GURL& origin) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->DeleteRegistration(
registration_id, origin, MakeStatusCallback(&was_called, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode GetAllRegistrationsInfos(
std::vector<ServiceWorkerRegistrationInfo>* registrations) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->GetAllRegistrationsInfos(
MakeGetRegistrationsInfosCallback(&was_called, &result, registrations));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode GetRegistrationsForOrigin(
const GURL& origin,
std::vector<scoped_refptr<ServiceWorkerRegistration>>* registrations) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->GetRegistrationsForOrigin(
origin,
MakeGetRegistrationsCallback(&was_called, &result, registrations));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode GetUserData(
int64_t registration_id,
const std::vector<std::string>& keys,
std::vector<std::string>* data) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->GetUserData(
registration_id, keys,
base::BindOnce(&GetUserDataCallback, &was_called, data, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode GetUserDataByKeyPrefix(
int64_t registration_id,
const std::string& key_prefix,
std::vector<std::string>* data) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->GetUserDataByKeyPrefix(
registration_id, key_prefix,
base::BindOnce(&GetUserDataCallback, &was_called, data, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode StoreUserData(
int64_t registration_id,
const GURL& origin,
const std::vector<std::pair<std::string, std::string>>& key_value_pairs) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->StoreUserData(registration_id, origin, key_value_pairs,
MakeStatusCallback(&was_called, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode ClearUserData(
int64_t registration_id,
const std::vector<std::string>& keys) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->ClearUserData(registration_id, keys,
MakeStatusCallback(&was_called, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode ClearUserDataByKeyPrefixes(
int64_t registration_id,
const std::vector<std::string>& key_prefixes) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->ClearUserDataByKeyPrefixes(
registration_id, key_prefixes,
MakeStatusCallback(&was_called, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode GetUserDataForAllRegistrations(
const std::string& key,
std::vector<std::pair<int64_t, std::string>>* data) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->GetUserDataForAllRegistrations(
key, base::BindOnce(&GetUserDataForAllRegistrationsCallback,
&was_called, data, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode ClearUserDataForAllRegistrationsByKeyPrefix(
const std::string& key_prefix) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->ClearUserDataForAllRegistrationsByKeyPrefix(
key_prefix, MakeStatusCallback(&was_called, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode UpdateToActiveState(
scoped_refptr<ServiceWorkerRegistration> registration) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->UpdateToActiveState(registration.get(),
MakeStatusCallback(&was_called, &result));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
void UpdateLastUpdateCheckTime(
scoped_refptr<ServiceWorkerRegistration> registration) {
storage()->UpdateLastUpdateCheckTime(registration.get());
base::RunLoop().RunUntilIdle();
}
blink::ServiceWorkerStatusCode FindRegistrationForDocument(
const GURL& document_url,
scoped_refptr<ServiceWorkerRegistration>* registration) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->FindRegistrationForDocument(
document_url, MakeFindCallback(&was_called, &result, registration));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode FindRegistrationForScope(
const GURL& scope,
scoped_refptr<ServiceWorkerRegistration>* registration) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->FindRegistrationForScope(
scope, MakeFindCallback(&was_called, &result, registration));
EXPECT_FALSE(was_called); // always async
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode FindRegistrationForId(
int64_t registration_id,
const GURL& origin,
scoped_refptr<ServiceWorkerRegistration>* registration) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->FindRegistrationForId(
registration_id, origin,
MakeFindCallback(&was_called, &result, registration));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
blink::ServiceWorkerStatusCode FindRegistrationForIdOnly(
int64_t registration_id,
scoped_refptr<ServiceWorkerRegistration>* registration) {
bool was_called = false;
base::Optional<blink::ServiceWorkerStatusCode> result;
storage()->FindRegistrationForIdOnly(
registration_id, MakeFindCallback(&was_called, &result, registration));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(was_called);
return result.value();
}
// Directly writes a registration using
// ServiceWorkerDatabase::WriteRegistration rather than
// ServiceWorkerStorage::StoreRegistration. Useful for simulating a
// registration written by an earlier version of Chrome.
void WriteRegistration(const RegistrationData& registration,
const std::vector<ResourceRecord>& resources) {
RegistrationData deleted_version;
std::vector<int64_t> newly_purgeable_resources;
ASSERT_EQ(
ServiceWorkerDatabase::STATUS_OK,
database()->WriteRegistration(registration, resources, &deleted_version,
&newly_purgeable_resources));
}
// user_data_directory_ must be declared first to preserve destructor order.
base::ScopedTempDir user_data_directory_;
base::FilePath user_data_directory_path_;
std::unique_ptr<EmbeddedWorkerTestHelper> helper_;
TestBrowserThreadBundle browser_thread_bundle_;
};
TEST_F(ServiceWorkerStorageTest, DisabledStorage) {
const GURL kScope("http://www.example.com/scope/");
const GURL kScript("http://www.example.com/script.js");
const GURL kDocumentUrl("http://www.example.com/scope/document.html");
const int64_t kRegistrationId = 0;
const int64_t kVersionId = 0;
const int64_t kResourceId = 0;
LazyInitialize();
storage()->Disable();
scoped_refptr<ServiceWorkerRegistration> found_registration;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
FindRegistrationForScope(kScope, &found_registration));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
FindRegistrationForId(kRegistrationId, kScope.GetOrigin(),
&found_registration));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
FindRegistrationForIdOnly(kRegistrationId, &found_registration));
EXPECT_FALSE(storage()->GetUninstallingRegistration(kScope.GetOrigin()));
std::vector<scoped_refptr<ServiceWorkerRegistration>> found_registrations;
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kErrorAbort,
GetRegistrationsForOrigin(kScope.GetOrigin(), &found_registrations));
std::vector<ServiceWorkerRegistrationInfo> all_registrations;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
GetAllRegistrationsInfos(&all_registrations));
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = kScope;
scoped_refptr<ServiceWorkerRegistration> live_registration =
new ServiceWorkerRegistration(options, kRegistrationId,
context()->AsWeakPtr());
scoped_refptr<ServiceWorkerVersion> live_version = new ServiceWorkerVersion(
live_registration.get(), kScript, blink::mojom::ScriptType::kClassic,
kVersionId, context()->AsWeakPtr());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
StoreRegistration(live_registration, live_version));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
UpdateToActiveState(live_registration));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
DeleteRegistration(kRegistrationId, kScope.GetOrigin()));
// Response reader and writer created by the disabled storage should fail to
// access the disk cache.
scoped_refptr<HttpResponseInfoIOBuffer> info_buffer =
new HttpResponseInfoIOBuffer();
EXPECT_EQ(net::ERR_CACHE_MISS,
ReadResponseInfo(storage(), kResourceId, info_buffer.get()));
EXPECT_EQ(net::ERR_FAILED, WriteBasicResponse(storage(), kResourceId));
EXPECT_EQ(net::ERR_FAILED,
WriteResponseMetadata(storage(), kResourceId, "foo"));
const std::string kUserDataKey = "key";
std::vector<std::string> user_data_out;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
GetUserData(kRegistrationId, {kUserDataKey}, &user_data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
GetUserDataByKeyPrefix(kRegistrationId, "prefix", &user_data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
StoreUserData(kRegistrationId, kScope.GetOrigin(),
{{kUserDataKey, "foo"}}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
ClearUserData(kRegistrationId, {kUserDataKey}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
ClearUserDataByKeyPrefixes(kRegistrationId, {"prefix"}));
std::vector<std::pair<int64_t, std::string>> data_list_out;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
GetUserDataForAllRegistrations(kUserDataKey, &data_list_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorAbort,
ClearUserDataForAllRegistrationsByKeyPrefix("prefix"));
// Next available ids should be invalid.
EXPECT_EQ(blink::mojom::kInvalidServiceWorkerRegistrationId,
storage()->NewRegistrationId());
EXPECT_EQ(blink::mojom::kInvalidServiceWorkerVersionId,
storage()->NewVersionId());
EXPECT_EQ(kInvalidServiceWorkerResourceId, storage()->NewRegistrationId());
}
TEST_F(ServiceWorkerStorageTest, StoreFindUpdateDeleteRegistration) {
const GURL kScope("http://www.test.not/scope/");
const GURL kDocumentUrl("http://www.test.not/scope/document.html");
const GURL kResource1("http://www.test.not/scope/resource1.js");
const int64_t kResource1Size = 1591234;
const GURL kResource2("http://www.test.not/scope/resource2.js");
const int64_t kResource2Size = 51;
const int64_t kRegistrationId = 0;
const int64_t kVersionId = 0;
const base::Time kToday = base::Time::Now();
const base::Time kYesterday = kToday - base::TimeDelta::FromDays(1);
std::set<blink::mojom::WebFeature> used_features = {
blink::mojom::WebFeature::kServiceWorkerControlledPage,
blink::mojom::WebFeature::kReferrerPolicyHeader,
blink::mojom::WebFeature::kLocationOrigin};
scoped_refptr<ServiceWorkerRegistration> found_registration;
// We shouldn't find anything without having stored anything.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForScope(kScope, &found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForId(kRegistrationId, kScope.GetOrigin(),
&found_registration));
EXPECT_FALSE(found_registration.get());
std::vector<ResourceRecord> resources;
resources.push_back(ResourceRecord(1, kResource1, kResource1Size));
resources.push_back(ResourceRecord(2, kResource2, kResource2Size));
// Store something.
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = kScope;
scoped_refptr<ServiceWorkerRegistration> live_registration =
new ServiceWorkerRegistration(options, kRegistrationId,
context()->AsWeakPtr());
scoped_refptr<ServiceWorkerVersion> live_version = new ServiceWorkerVersion(
live_registration.get(), kResource1, blink::mojom::ScriptType::kClassic,
kVersionId, context()->AsWeakPtr());
live_version->set_fetch_handler_existence(
ServiceWorkerVersion::FetchHandlerExistence::EXISTS);
live_version->SetStatus(ServiceWorkerVersion::INSTALLED);
live_version->script_cache_map()->SetResources(resources);
live_version->set_used_features(
std::set<blink::mojom::WebFeature>(used_features));
live_registration->SetWaitingVersion(live_version);
live_registration->set_last_update_check(kYesterday);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(live_registration, live_version));
// Now we should find it and get the live ptr back immediately.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_EQ(live_registration, found_registration);
EXPECT_EQ(kResource1Size + kResource2Size,
live_registration->resources_total_size_bytes());
EXPECT_EQ(kResource1Size + kResource2Size,
found_registration->resources_total_size_bytes());
EXPECT_EQ(used_features,
found_registration->waiting_version()->used_features());
found_registration = nullptr;
// But FindRegistrationForScope is always async.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForScope(kScope, &found_registration));
EXPECT_EQ(live_registration, found_registration);
found_registration = nullptr;
// Can be found by id too.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForId(kRegistrationId, kScope.GetOrigin(),
&found_registration));
ASSERT_TRUE(found_registration.get());
EXPECT_EQ(kRegistrationId, found_registration->id());
EXPECT_EQ(live_registration, found_registration);
found_registration = nullptr;
// Can be found by just the id too.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForIdOnly(kRegistrationId, &found_registration));
ASSERT_TRUE(found_registration.get());
EXPECT_EQ(kRegistrationId, found_registration->id());
EXPECT_EQ(live_registration, found_registration);
found_registration = nullptr;
// Drop the live registration, but keep the version live.
live_registration = nullptr;
// Now FindRegistrationForDocument should be async.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
ASSERT_TRUE(found_registration.get());
EXPECT_EQ(kRegistrationId, found_registration->id());
EXPECT_TRUE(found_registration->HasOneRef());
// Check that sizes are populated correctly
EXPECT_EQ(live_version.get(), found_registration->waiting_version());
EXPECT_EQ(kResource1Size + kResource2Size,
found_registration->resources_total_size_bytes());
std::vector<ServiceWorkerRegistrationInfo> all_registrations;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetAllRegistrationsInfos(&all_registrations));
EXPECT_EQ(1u, all_registrations.size());
ServiceWorkerRegistrationInfo info = all_registrations[0];
EXPECT_EQ(kResource1Size + kResource2Size, info.stored_version_size_bytes);
all_registrations.clear();
// Finding by origin should provide the same result if origin is kScope.
std::vector<scoped_refptr<ServiceWorkerRegistration>>
registrations_for_origin;
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(kScope.GetOrigin(), &registrations_for_origin));
EXPECT_EQ(1u, registrations_for_origin.size());
registrations_for_origin.clear();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(GURL("http://example.com/").GetOrigin(),
&registrations_for_origin));
EXPECT_TRUE(registrations_for_origin.empty());
found_registration = nullptr;
// Drop the live version too.
live_version = nullptr;
// And FindRegistrationForScope is always async.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForScope(kScope, &found_registration));
ASSERT_TRUE(found_registration.get());
EXPECT_EQ(kRegistrationId, found_registration->id());
EXPECT_TRUE(found_registration->HasOneRef());
EXPECT_FALSE(found_registration->active_version());
ASSERT_TRUE(found_registration->waiting_version());
EXPECT_EQ(kYesterday, found_registration->last_update_check());
EXPECT_EQ(ServiceWorkerVersion::INSTALLED,
found_registration->waiting_version()->status());
// Update to active and update the last check time.
scoped_refptr<ServiceWorkerVersion> temp_version =
found_registration->waiting_version();
temp_version->SetStatus(ServiceWorkerVersion::ACTIVATED);
found_registration->SetActiveVersion(temp_version);
temp_version = nullptr;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
UpdateToActiveState(found_registration));
found_registration->set_last_update_check(kToday);
UpdateLastUpdateCheckTime(found_registration.get());
found_registration = nullptr;
// Trying to update a unstored registration to active should fail.
scoped_refptr<ServiceWorkerRegistration> unstored_registration =
new ServiceWorkerRegistration(options, kRegistrationId + 1,
context()->AsWeakPtr());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
UpdateToActiveState(unstored_registration));
unstored_registration = nullptr;
// The Find methods should return a registration with an active version
// and the expected update time.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
ASSERT_TRUE(found_registration.get());
EXPECT_EQ(kRegistrationId, found_registration->id());
EXPECT_TRUE(found_registration->HasOneRef());
EXPECT_FALSE(found_registration->waiting_version());
ASSERT_TRUE(found_registration->active_version());
EXPECT_EQ(ServiceWorkerVersion::ACTIVATED,
found_registration->active_version()->status());
EXPECT_EQ(kToday, found_registration->last_update_check());
// Delete from storage but with a instance still live.
EXPECT_TRUE(context()->GetLiveVersion(kRegistrationId));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
DeleteRegistration(kRegistrationId, kScope.GetOrigin()));
EXPECT_TRUE(context()->GetLiveVersion(kRegistrationId));
// Should no longer be found.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForId(kRegistrationId, kScope.GetOrigin(),
&found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForIdOnly(kRegistrationId, &found_registration));
EXPECT_FALSE(found_registration.get());
// Deleting an unstored registration should succeed.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
DeleteRegistration(kRegistrationId + 1, kScope.GetOrigin()));
}
TEST_F(ServiceWorkerStorageTest, InstallingRegistrationsAreFindable) {
const GURL kScope("http://www.test.not/scope/");
const GURL kScript("http://www.test.not/script.js");
const GURL kDocumentUrl("http://www.test.not/scope/document.html");
const int64_t kRegistrationId = 0;
const int64_t kVersionId = 0;
scoped_refptr<ServiceWorkerRegistration> found_registration;
// Create an unstored registration.
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = kScope;
scoped_refptr<ServiceWorkerRegistration> live_registration =
new ServiceWorkerRegistration(options, kRegistrationId,
context()->AsWeakPtr());
scoped_refptr<ServiceWorkerVersion> live_version = new ServiceWorkerVersion(
live_registration.get(), kScript, blink::mojom::ScriptType::kClassic,
kVersionId, context()->AsWeakPtr());
live_version->SetStatus(ServiceWorkerVersion::INSTALLING);
live_registration->SetWaitingVersion(live_version);
// Should not be findable, including by GetAllRegistrationsInfos.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForId(kRegistrationId, kScope.GetOrigin(),
&found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForIdOnly(kRegistrationId, &found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForScope(kScope, &found_registration));
EXPECT_FALSE(found_registration.get());
std::vector<ServiceWorkerRegistrationInfo> all_registrations;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetAllRegistrationsInfos(&all_registrations));
EXPECT_TRUE(all_registrations.empty());
std::vector<scoped_refptr<ServiceWorkerRegistration>>
registrations_for_origin;
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(kScope.GetOrigin(), &registrations_for_origin));
EXPECT_TRUE(registrations_for_origin.empty());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(GURL("http://example.com/").GetOrigin(),
&registrations_for_origin));
EXPECT_TRUE(registrations_for_origin.empty());
// Notify storage of it being installed.
storage()->NotifyInstallingRegistration(live_registration.get());
// Now should be findable.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForId(kRegistrationId, kScope.GetOrigin(),
&found_registration));
EXPECT_EQ(live_registration, found_registration);
found_registration = nullptr;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForIdOnly(kRegistrationId, &found_registration));
EXPECT_EQ(live_registration, found_registration);
found_registration = nullptr;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_EQ(live_registration, found_registration);
found_registration = nullptr;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForScope(kScope, &found_registration));
EXPECT_EQ(live_registration, found_registration);
found_registration = nullptr;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetAllRegistrationsInfos(&all_registrations));
EXPECT_EQ(1u, all_registrations.size());
all_registrations.clear();
// Finding by origin should provide the same result if origin is kScope.
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(kScope.GetOrigin(), &registrations_for_origin));
EXPECT_EQ(1u, registrations_for_origin.size());
registrations_for_origin.clear();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(GURL("http://example.com/").GetOrigin(),
&registrations_for_origin));
EXPECT_TRUE(registrations_for_origin.empty());
// Notify storage of installation no longer happening.
storage()->NotifyDoneInstallingRegistration(
live_registration.get(), nullptr, blink::ServiceWorkerStatusCode::kOk);
// Once again, should not be findable.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForId(kRegistrationId, kScope.GetOrigin(),
&found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForIdOnly(kRegistrationId, &found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
FindRegistrationForScope(kScope, &found_registration));
EXPECT_FALSE(found_registration.get());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetAllRegistrationsInfos(&all_registrations));
EXPECT_TRUE(all_registrations.empty());
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(kScope.GetOrigin(), &registrations_for_origin));
EXPECT_TRUE(registrations_for_origin.empty());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetRegistrationsForOrigin(GURL("http://example.com/").GetOrigin(),
&registrations_for_origin));
EXPECT_TRUE(registrations_for_origin.empty());
}
TEST_F(ServiceWorkerStorageTest, StoreUserData) {
const GURL kScope("http://www.test.not/scope/");
const GURL kScript("http://www.test.not/script.js");
LazyInitialize();
// Store a registration.
scoped_refptr<ServiceWorkerRegistration> live_registration =
CreateLiveRegistrationAndVersion(kScope, kScript);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(live_registration,
live_registration->waiting_version()));
const int64_t kRegistrationId = live_registration->id();
// Store user data associated with the registration.
std::vector<std::string> data_out;
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kOk,
StoreUserData(kRegistrationId, kScope.GetOrigin(), {{"key", "data"}}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserData(kRegistrationId, {"key"}, &data_out));
ASSERT_EQ(1u, data_out.size());
EXPECT_EQ("data", data_out[0]);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"unknown_key"}, &data_out));
std::vector<std::pair<int64_t, std::string>> data_list_out;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserDataForAllRegistrations("key", &data_list_out));
ASSERT_EQ(1u, data_list_out.size());
EXPECT_EQ(kRegistrationId, data_list_out[0].first);
EXPECT_EQ("data", data_list_out[0].second);
data_list_out.clear();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserDataForAllRegistrations("unknown_key", &data_list_out));
EXPECT_EQ(0u, data_list_out.size());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
ClearUserData(kRegistrationId, {"key"}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key"}, &data_out));
// Write/overwrite multiple user data keys.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreUserData(
kRegistrationId, kScope.GetOrigin(),
{{"key", "overwrite"}, {"key3", "data3"}, {"key4", "data4"}}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key2"}, &data_out));
EXPECT_TRUE(data_out.empty());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserData(kRegistrationId, {"key", "key3", "key4"}, &data_out));
ASSERT_EQ(3u, data_out.size());
EXPECT_EQ("overwrite", data_out[0]);
EXPECT_EQ("data3", data_out[1]);
EXPECT_EQ("data4", data_out[2]);
// Multiple gets fail if one is not found.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key", "key2"}, &data_out));
EXPECT_TRUE(data_out.empty());
// Delete multiple user data keys, even if some are not found.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
ClearUserData(kRegistrationId, {"key", "key2", "key3"}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key"}, &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key2"}, &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key3"}, &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserData(kRegistrationId, {"key4"}, &data_out));
ASSERT_EQ(1u, data_out.size());
EXPECT_EQ("data4", data_out[0]);
// Get/delete multiple user data keys by prefixes.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreUserData(kRegistrationId, kScope.GetOrigin(),
{{"prefixA", "data1"},
{"prefixA2", "data2"},
{"prefixB", "data3"},
{"prefixC", "data4"}}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserDataByKeyPrefix(kRegistrationId, "prefix", &data_out));
ASSERT_EQ(4u, data_out.size());
EXPECT_EQ("data1", data_out[0]);
EXPECT_EQ("data2", data_out[1]);
EXPECT_EQ("data3", data_out[2]);
EXPECT_EQ("data4", data_out[3]);
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kOk,
ClearUserDataByKeyPrefixes(kRegistrationId, {"prefixA", "prefixC"}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserDataByKeyPrefix(kRegistrationId, "prefix", &data_out));
ASSERT_EQ(1u, data_out.size());
EXPECT_EQ("data3", data_out[0]);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
ClearUserDataForAllRegistrationsByKeyPrefix("prefixB"));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserDataByKeyPrefix(kRegistrationId, "prefix", &data_out));
EXPECT_TRUE(data_out.empty());
// User data should be deleted when the associated registration is deleted.
ASSERT_EQ(
blink::ServiceWorkerStatusCode::kOk,
StoreUserData(kRegistrationId, kScope.GetOrigin(), {{"key", "data"}}));
ASSERT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserData(kRegistrationId, {"key"}, &data_out));
ASSERT_EQ(1u, data_out.size());
ASSERT_EQ("data", data_out[0]);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
DeleteRegistration(kRegistrationId, kScope.GetOrigin()));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key"}, &data_out));
data_list_out.clear();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserDataForAllRegistrations("key", &data_list_out));
EXPECT_EQ(0u, data_list_out.size());
// Data access with an invalid registration id should be failed.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
StoreUserData(blink::mojom::kInvalidServiceWorkerRegistrationId,
kScope.GetOrigin(), {{"key", "data"}}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
GetUserData(blink::mojom::kInvalidServiceWorkerRegistrationId,
{"key"}, &data_out));
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kErrorFailed,
GetUserDataByKeyPrefix(blink::mojom::kInvalidServiceWorkerRegistrationId,
"prefix", &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserData(blink::mojom::kInvalidServiceWorkerRegistrationId,
{"key"}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserDataByKeyPrefixes(
blink::mojom::kInvalidServiceWorkerRegistrationId, {"prefix"}));
// Data access with an empty key should be failed.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
StoreUserData(kRegistrationId, kScope.GetOrigin(),
std::vector<std::pair<std::string, std::string>>()));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
StoreUserData(kRegistrationId, kScope.GetOrigin(),
{{std::string(), "data"}}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
StoreUserData(kRegistrationId, kScope.GetOrigin(),
{{std::string(), "data"}, {"key", "data"}}));
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kErrorFailed,
GetUserData(kRegistrationId, std::vector<std::string>(), &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
GetUserDataByKeyPrefix(kRegistrationId, std::string(), &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
GetUserData(kRegistrationId, {std::string()}, &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
GetUserData(kRegistrationId, {std::string(), "key"}, &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserData(kRegistrationId, std::vector<std::string>()));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserData(kRegistrationId, {std::string()}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserData(kRegistrationId, {std::string(), "key"}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserDataByKeyPrefixes(kRegistrationId, {}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserDataByKeyPrefixes(kRegistrationId, {std::string()}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
ClearUserDataForAllRegistrationsByKeyPrefix(std::string()));
data_list_out.clear();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed,
GetUserDataForAllRegistrations(std::string(), &data_list_out));
}
// The *_BeforeInitialize tests exercise the API before LazyInitialize() is
// called.
TEST_F(ServiceWorkerStorageTest, StoreUserData_BeforeInitialize) {
const int kRegistrationId = 0;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
StoreUserData(kRegistrationId, GURL("https://example.com"),
{{"key", "data"}}));
}
TEST_F(ServiceWorkerStorageTest, GetUserData_BeforeInitialize) {
const int kRegistrationId = 0;
std::vector<std::string> data_out;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserData(kRegistrationId, {"key"}, &data_out));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorNotFound,
GetUserDataByKeyPrefix(kRegistrationId, "prefix", &data_out));
}
TEST_F(ServiceWorkerStorageTest, ClearUserData_BeforeInitialize) {
const int kRegistrationId = 0;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
ClearUserData(kRegistrationId, {"key"}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
ClearUserDataByKeyPrefixes(kRegistrationId, {"prefix"}));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
ClearUserDataForAllRegistrationsByKeyPrefix("key"));
}
TEST_F(ServiceWorkerStorageTest,
GetUserDataForAllRegistrations_BeforeInitialize) {
std::vector<std::pair<int64_t, std::string>> data_list_out;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
GetUserDataForAllRegistrations("key", &data_list_out));
EXPECT_TRUE(data_list_out.empty());
}
class ServiceWorkerResourceStorageTest : public ServiceWorkerStorageTest {
public:
void SetUp() override {
ServiceWorkerStorageTest::SetUp();
LazyInitialize();
scope_ = GURL("http://www.test.not/scope/");
script_ = GURL("http://www.test.not/script.js");
import_ = GURL("http://www.test.not/import.js");
document_url_ = GURL("http://www.test.not/scope/document.html");
registration_id_ = storage()->NewRegistrationId();
version_id_ = storage()->NewVersionId();
resource_id1_ = storage()->NewResourceId();
resource_id2_ = storage()->NewResourceId();
resource_id1_size_ = 239193;
resource_id2_size_ = 59923;
// Cons up a new registration+version with two script resources.
RegistrationData data;
data.registration_id = registration_id_;
data.scope = scope_;
data.script = script_;
data.version_id = version_id_;
data.is_active = false;
std::vector<ResourceRecord> resources;
resources.push_back(
ResourceRecord(resource_id1_, script_, resource_id1_size_));
resources.push_back(
ResourceRecord(resource_id2_, import_, resource_id2_size_));
registration_ = storage()->GetOrCreateRegistration(data, resources);
registration_->waiting_version()->SetStatus(ServiceWorkerVersion::NEW);
// Add the resources ids to the uncommitted list.
storage()->StoreUncommittedResourceId(resource_id1_);
storage()->StoreUncommittedResourceId(resource_id2_);
base::RunLoop().RunUntilIdle();
std::set<int64_t> verify_ids;
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetUncommittedResourceIds(&verify_ids));
EXPECT_EQ(2u, verify_ids.size());
// And dump something in the disk cache for them.
WriteBasicResponse(storage(), resource_id1_);
WriteBasicResponse(storage(), resource_id2_);
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id2_, true));
// Storing the registration/version should take the resources ids out
// of the uncommitted list.
EXPECT_EQ(
blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(registration_, registration_->waiting_version()));
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetUncommittedResourceIds(&verify_ids));
EXPECT_TRUE(verify_ids.empty());
}
protected:
GURL scope_;
GURL script_;
GURL import_;
GURL document_url_;
int64_t registration_id_;
int64_t version_id_;
int64_t resource_id1_;
uint64_t resource_id1_size_;
int64_t resource_id2_;
uint64_t resource_id2_size_;
scoped_refptr<ServiceWorkerRegistration> registration_;
};
class ServiceWorkerResourceStorageDiskTest
: public ServiceWorkerResourceStorageTest {
public:
void SetUp() override {
ASSERT_TRUE(InitUserDataDirectory());
ServiceWorkerResourceStorageTest::SetUp();
}
};
TEST_F(ServiceWorkerResourceStorageTest,
WriteMetadataWithServiceWorkerResponseMetadataWriter) {
const char kMetadata1[] = "Test metadata";
const char kMetadata2[] = "small";
int64_t new_resource_id_ = storage()->NewResourceId();
// Writing metadata to nonexistent resoirce ID must fail.
EXPECT_GE(0, WriteResponseMetadata(storage(), new_resource_id_, kMetadata1));
// Check metadata is written.
EXPECT_EQ(static_cast<int>(strlen(kMetadata1)),
WriteResponseMetadata(storage(), resource_id1_, kMetadata1));
EXPECT_TRUE(VerifyResponseMetadata(storage(), resource_id1_, kMetadata1));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
// Check metadata is written and truncated.
EXPECT_EQ(static_cast<int>(strlen(kMetadata2)),
WriteResponseMetadata(storage(), resource_id1_, kMetadata2));
EXPECT_TRUE(VerifyResponseMetadata(storage(), resource_id1_, kMetadata2));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
// Check metadata is deleted.
EXPECT_EQ(0, WriteResponseMetadata(storage(), resource_id1_, ""));
EXPECT_FALSE(VerifyResponseMetadata(storage(), resource_id1_, ""));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
}
TEST_F(ServiceWorkerResourceStorageTest,
WriteMetadataWithServiceWorkerScriptCacheMap) {
const char kMetadata1[] = "Test metadata";
const char kMetadata2[] = "small";
ServiceWorkerVersion* version = registration_->waiting_version();
EXPECT_TRUE(version);
// Writing metadata to nonexistent URL must fail.
EXPECT_GE(0,
WriteMetadata(version, GURL("http://www.test.not/nonexistent.js"),
kMetadata1));
// Clearing metadata of nonexistent URL must fail.
EXPECT_GE(0,
ClearMetadata(version, GURL("http://www.test.not/nonexistent.js")));
// Check metadata is written.
EXPECT_EQ(static_cast<int>(strlen(kMetadata1)),
WriteMetadata(version, script_, kMetadata1));
EXPECT_TRUE(VerifyResponseMetadata(storage(), resource_id1_, kMetadata1));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
// Check metadata is written and truncated.
EXPECT_EQ(static_cast<int>(strlen(kMetadata2)),
WriteMetadata(version, script_, kMetadata2));
EXPECT_TRUE(VerifyResponseMetadata(storage(), resource_id1_, kMetadata2));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
// Check metadata is deleted.
EXPECT_EQ(0, ClearMetadata(version, script_));
EXPECT_FALSE(VerifyResponseMetadata(storage(), resource_id1_, ""));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
}
TEST_F(ServiceWorkerResourceStorageTest, DeleteRegistration_NoLiveVersion) {
bool was_called = false;
blink::ServiceWorkerStatusCode result =
blink::ServiceWorkerStatusCode::kErrorFailed;
std::set<int64_t> verify_ids;
registration_->SetWaitingVersion(nullptr);
registration_ = nullptr;
// Deleting the registration should result in the resources being added to the
// purgeable list and then doomed in the disk cache and removed from that
// list.
storage()->DeleteRegistration(
registration_id_, scope_.GetOrigin(),
base::BindOnce(&VerifyPurgeableListStatusCallback,
base::Unretained(database()), &verify_ids, &was_called,
&result));
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(was_called);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result);
EXPECT_EQ(2u, verify_ids.size());
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_TRUE(verify_ids.empty());
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id1_, false));
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id2_, false));
}
TEST_F(ServiceWorkerResourceStorageTest, DeleteRegistration_WaitingVersion) {
bool was_called = false;
blink::ServiceWorkerStatusCode result =
blink::ServiceWorkerStatusCode::kErrorFailed;
std::set<int64_t> verify_ids;
// Deleting the registration should result in the resources being added to the
// purgeable list and then doomed in the disk cache and removed from that
// list.
storage()->DeleteRegistration(
registration_->id(), scope_.GetOrigin(),
base::BindOnce(&VerifyPurgeableListStatusCallback,
base::Unretained(database()), &verify_ids, &was_called,
&result));
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(was_called);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result);
EXPECT_EQ(2u, verify_ids.size());
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_EQ(2u, verify_ids.size());
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, false));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id2_, false));
// Doom the version, now it happens.
registration_->waiting_version()->Doom();
base::RunLoop().RunUntilIdle();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result);
EXPECT_EQ(2u, verify_ids.size());
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_TRUE(verify_ids.empty());
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id1_, false));
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id2_, false));
}
TEST_F(ServiceWorkerResourceStorageTest, DeleteRegistration_ActiveVersion) {
// Promote the worker to active and add a controllee.
registration_->SetActiveVersion(registration_->waiting_version());
storage()->UpdateToActiveState(registration_.get(), base::DoNothing());
ServiceWorkerRemoteProviderEndpoint remote_endpoint;
base::WeakPtr<ServiceWorkerProviderHost> host = CreateProviderHostForWindow(
33 /* dummy render process id */, true /* is_parent_frame_secure */,
context()->AsWeakPtr(), &remote_endpoint);
registration_->active_version()->AddControllee(host.get());
bool was_called = false;
blink::ServiceWorkerStatusCode result =
blink::ServiceWorkerStatusCode::kErrorFailed;
std::set<int64_t> verify_ids;
// Deleting the registration should move the resources to the purgeable list
// but keep them available.
storage()->DeleteRegistration(
registration_->id(), scope_.GetOrigin(),
base::BindOnce(&VerifyPurgeableListStatusCallback,
base::Unretained(database()), &verify_ids, &was_called,
&result));
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(was_called);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result);
EXPECT_EQ(2u, verify_ids.size());
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_EQ(2u, verify_ids.size());
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id2_, true));
// Removing the controllee should cause the resources to be deleted.
registration_->active_version()->RemoveControllee(host->client_uuid());
registration_->active_version()->Doom();
base::RunLoop().RunUntilIdle();
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_TRUE(verify_ids.empty());
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id1_, false));
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id2_, false));
}
TEST_F(ServiceWorkerResourceStorageDiskTest, CleanupOnRestart) {
// Promote the worker to active and add a controllee.
registration_->SetActiveVersion(registration_->waiting_version());
registration_->SetWaitingVersion(nullptr);
storage()->UpdateToActiveState(registration_.get(), base::DoNothing());
ServiceWorkerRemoteProviderEndpoint remote_endpoint;
base::WeakPtr<ServiceWorkerProviderHost> host = CreateProviderHostForWindow(
33 /* dummy render process id */, true /* is_parent_frame_secure */,
context()->AsWeakPtr(), &remote_endpoint);
registration_->active_version()->AddControllee(host.get());
bool was_called = false;
blink::ServiceWorkerStatusCode result =
blink::ServiceWorkerStatusCode::kErrorFailed;
std::set<int64_t> verify_ids;
// Deleting the registration should move the resources to the purgeable list
// but keep them available.
storage()->DeleteRegistration(
registration_->id(), scope_.GetOrigin(),
base::BindOnce(&VerifyPurgeableListStatusCallback,
base::Unretained(database()), &verify_ids, &was_called,
&result));
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(was_called);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result);
EXPECT_EQ(2u, verify_ids.size());
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_EQ(2u, verify_ids.size());
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, true));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id2_, true));
// Also add an uncommitted resource.
int64_t kStaleUncommittedResourceId = storage()->NewResourceId();
storage()->StoreUncommittedResourceId(kStaleUncommittedResourceId);
base::RunLoop().RunUntilIdle();
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetUncommittedResourceIds(&verify_ids));
EXPECT_EQ(1u, verify_ids.size());
WriteBasicResponse(storage(), kStaleUncommittedResourceId);
EXPECT_TRUE(
VerifyBasicResponse(storage(), kStaleUncommittedResourceId, true));
// Simulate browser shutdown. The purgeable and uncommitted resources are now
// stale.
InitializeTestHelper();
LazyInitialize();
// Store a new uncommitted resource. This triggers stale resource cleanup.
int64_t kNewResourceId = storage()->NewResourceId();
WriteBasicResponse(storage(), kNewResourceId);
storage()->StoreUncommittedResourceId(kNewResourceId);
base::RunLoop().RunUntilIdle();
// The stale resources should be purged, but the new resource should persist.
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetUncommittedResourceIds(&verify_ids));
ASSERT_EQ(1u, verify_ids.size());
EXPECT_EQ(kNewResourceId, *verify_ids.begin());
// Purging resources needs interactions with SimpleCache's worker thread,
// so single RunUntilIdle() call may not be sufficient.
while (storage()->is_purge_pending_)
base::RunLoop().RunUntilIdle();
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_TRUE(verify_ids.empty());
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id1_, false));
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id2_, false));
EXPECT_FALSE(
VerifyBasicResponse(storage(), kStaleUncommittedResourceId, false));
EXPECT_TRUE(VerifyBasicResponse(storage(), kNewResourceId, true));
}
TEST_F(ServiceWorkerResourceStorageDiskTest, DeleteAndStartOver) {
EXPECT_FALSE(storage()->IsDisabled());
ASSERT_TRUE(base::DirectoryExists(storage()->GetDiskCachePath()));
ASSERT_TRUE(base::DirectoryExists(storage()->GetDatabasePath()));
base::RunLoop run_loop;
blink::ServiceWorkerStatusCode status =
blink::ServiceWorkerStatusCode::kErrorFailed;
storage()->DeleteAndStartOver(
base::BindOnce(&StatusAndQuitCallback, &status, run_loop.QuitClosure()));
run_loop.Run();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status);
EXPECT_TRUE(storage()->IsDisabled());
EXPECT_FALSE(base::DirectoryExists(storage()->GetDiskCachePath()));
EXPECT_FALSE(base::DirectoryExists(storage()->GetDatabasePath()));
}
TEST_F(ServiceWorkerResourceStorageDiskTest,
DeleteAndStartOver_UnrelatedFileExists) {
EXPECT_FALSE(storage()->IsDisabled());
ASSERT_TRUE(base::DirectoryExists(storage()->GetDiskCachePath()));
ASSERT_TRUE(base::DirectoryExists(storage()->GetDatabasePath()));
// Create an unrelated file in the database directory to make sure such a file
// does not prevent DeleteAndStartOver.
base::FilePath file_path;
ASSERT_TRUE(
base::CreateTemporaryFileInDir(storage()->GetDatabasePath(), &file_path));
ASSERT_TRUE(base::PathExists(file_path));
base::RunLoop run_loop;
blink::ServiceWorkerStatusCode status =
blink::ServiceWorkerStatusCode::kErrorFailed;
storage()->DeleteAndStartOver(
base::BindOnce(&StatusAndQuitCallback, &status, run_loop.QuitClosure()));
run_loop.Run();
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status);
EXPECT_TRUE(storage()->IsDisabled());
EXPECT_FALSE(base::DirectoryExists(storage()->GetDiskCachePath()));
EXPECT_FALSE(base::DirectoryExists(storage()->GetDatabasePath()));
}
TEST_F(ServiceWorkerResourceStorageDiskTest,
DeleteAndStartOver_OpenedFileExists) {
EXPECT_FALSE(storage()->IsDisabled());
ASSERT_TRUE(base::DirectoryExists(storage()->GetDiskCachePath()));
ASSERT_TRUE(base::DirectoryExists(storage()->GetDatabasePath()));
// Create an unrelated opened file in the database directory to make sure such
// a file does not prevent DeleteAndStartOver on non-Windows platforms.
base::FilePath file_path;
base::ScopedFILE file(base::CreateAndOpenTemporaryFileInDir(
storage()->GetDatabasePath(), &file_path));
ASSERT_TRUE(file);
ASSERT_TRUE(base::PathExists(file_path));
base::RunLoop run_loop;
blink::ServiceWorkerStatusCode status =
blink::ServiceWorkerStatusCode::kErrorNotFound;
storage()->DeleteAndStartOver(
base::BindOnce(&StatusAndQuitCallback, &status, run_loop.QuitClosure()));
run_loop.Run();
#if defined(OS_WIN)
// On Windows, deleting the directory containing an opened file should fail.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kErrorFailed, status);
EXPECT_TRUE(storage()->IsDisabled());
EXPECT_TRUE(base::DirectoryExists(storage()->GetDiskCachePath()));
EXPECT_TRUE(base::DirectoryExists(storage()->GetDatabasePath()));
#else
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status);
EXPECT_TRUE(storage()->IsDisabled());
EXPECT_FALSE(base::DirectoryExists(storage()->GetDiskCachePath()));
EXPECT_FALSE(base::DirectoryExists(storage()->GetDatabasePath()));
#endif
}
TEST_F(ServiceWorkerResourceStorageTest, UpdateRegistration) {
// Promote the worker to active worker and add a controllee.
registration_->SetActiveVersion(registration_->waiting_version());
storage()->UpdateToActiveState(registration_.get(), base::DoNothing());
ServiceWorkerRemoteProviderEndpoint remote_endpoint;
base::WeakPtr<ServiceWorkerProviderHost> host = CreateProviderHostForWindow(
33 /* dummy render process id */, true /* is_parent_frame_secure */,
helper_->context()->AsWeakPtr(), &remote_endpoint);
registration_->active_version()->AddControllee(host.get());
bool was_called = false;
blink::ServiceWorkerStatusCode result =
blink::ServiceWorkerStatusCode::kErrorFailed;
std::set<int64_t> verify_ids;
// Make an updated registration.
scoped_refptr<ServiceWorkerVersion> live_version = new ServiceWorkerVersion(
registration_.get(), script_, blink::mojom::ScriptType::kClassic,
storage()->NewVersionId(), context()->AsWeakPtr());
live_version->SetStatus(ServiceWorkerVersion::NEW);
registration_->SetWaitingVersion(live_version);
std::vector<ResourceRecord> records;
records.push_back(ResourceRecord(10, live_version->script_url(), 100));
live_version->script_cache_map()->SetResources(records);
live_version->set_fetch_handler_existence(
ServiceWorkerVersion::FetchHandlerExistence::EXISTS);
// Writing the registration should move the old version's resources to the
// purgeable list but keep them available.
storage()->StoreRegistration(
registration_.get(), registration_->waiting_version(),
base::BindOnce(&VerifyPurgeableListStatusCallback,
base::Unretained(database()), &verify_ids, &was_called,
&result));
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(was_called);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, result);
EXPECT_EQ(2u, verify_ids.size());
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_EQ(2u, verify_ids.size());
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id1_, false));
EXPECT_TRUE(VerifyBasicResponse(storage(), resource_id2_, false));
// Removing the controllee should cause the old version's resources to be
// deleted.
registration_->active_version()->RemoveControllee(host->client_uuid());
registration_->active_version()->Doom();
base::RunLoop().RunUntilIdle();
verify_ids.clear();
EXPECT_EQ(ServiceWorkerDatabase::STATUS_OK,
database()->GetPurgeableResourceIds(&verify_ids));
EXPECT_TRUE(verify_ids.empty());
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id1_, false));
EXPECT_FALSE(VerifyBasicResponse(storage(), resource_id2_, false));
}
TEST_F(ServiceWorkerStorageTest, FindRegistration_LongestScopeMatch) {
LazyInitialize();
const GURL kDocumentUrl("http://www.example.com/scope/foo");
scoped_refptr<ServiceWorkerRegistration> found_registration;
// Registration for "/scope/".
const GURL kScope1("http://www.example.com/scope/");
const GURL kScript1("http://www.example.com/script1.js");
scoped_refptr<ServiceWorkerRegistration> live_registration1 =
CreateLiveRegistrationAndVersion(kScope1, kScript1);
// Registration for "/scope/foo".
const GURL kScope2("http://www.example.com/scope/foo");
const GURL kScript2("http://www.example.com/script2.js");
scoped_refptr<ServiceWorkerRegistration> live_registration2 =
CreateLiveRegistrationAndVersion(kScope2, kScript2);
// Registration for "/scope/foobar".
const GURL kScope3("http://www.example.com/scope/foobar");
const GURL kScript3("http://www.example.com/script3.js");
scoped_refptr<ServiceWorkerRegistration> live_registration3 =
CreateLiveRegistrationAndVersion(kScope3, kScript3);
// Notify storage of them being installed.
storage()->NotifyInstallingRegistration(live_registration1.get());
storage()->NotifyInstallingRegistration(live_registration2.get());
storage()->NotifyInstallingRegistration(live_registration3.get());
// Find a registration among installing ones.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_EQ(live_registration2, found_registration);
found_registration = nullptr;
// Store registrations.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(live_registration1,
live_registration1->waiting_version()));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(live_registration2,
live_registration2->waiting_version()));
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(live_registration3,
live_registration3->waiting_version()));
// Notify storage of installations no longer happening.
storage()->NotifyDoneInstallingRegistration(
live_registration1.get(), nullptr, blink::ServiceWorkerStatusCode::kOk);
storage()->NotifyDoneInstallingRegistration(
live_registration2.get(), nullptr, blink::ServiceWorkerStatusCode::kOk);
storage()->NotifyDoneInstallingRegistration(
live_registration3.get(), nullptr, blink::ServiceWorkerStatusCode::kOk);
// Find a registration among installed ones.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kDocumentUrl, &found_registration));
EXPECT_EQ(live_registration2, found_registration);
}
// Test fixture that uses disk storage, rather than memory. Useful for tests
// that test persistence by simulating browser shutdown and restart.
class ServiceWorkerStorageDiskTest : public ServiceWorkerStorageTest {
public:
void SetUp() override {
ASSERT_TRUE(InitUserDataDirectory());
ServiceWorkerStorageTest::SetUp();
}
};
TEST_F(ServiceWorkerStorageTest, OriginTrialsAbsentEntryAndEmptyEntry) {
const GURL origin1("http://www1.example.com");
const GURL scope1("http://www1.example.com/foo/");
RegistrationData data1;
data1.registration_id = 100;
data1.scope = scope1;
data1.script = GURL(origin1.spec() + "/script.js");
data1.version_id = 1000;
data1.is_active = true;
data1.resources_total_size_bytes = 100;
// Don't set origin_trial_tokens to simulate old database entry.
std::vector<ResourceRecord> resources1;
resources1.push_back(ResourceRecord(1, data1.script, 100));
WriteRegistration(data1, resources1);
const GURL origin2("http://www2.example.com");
const GURL scope2("http://www2.example.com/foo/");
RegistrationData data2;
data2.registration_id = 200;
data2.scope = scope2;
data2.script = GURL(origin2.spec() + "/script.js");
data2.version_id = 2000;
data2.is_active = true;
data2.resources_total_size_bytes = 200;
// Set empty origin_trial_tokens.
data2.origin_trial_tokens = blink::TrialTokenValidator::FeatureToTokensMap();
std::vector<ResourceRecord> resources2;
resources2.push_back(ResourceRecord(2, data2.script, 200));
WriteRegistration(data2, resources2);
scoped_refptr<ServiceWorkerRegistration> found_registration;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(scope1, &found_registration));
ASSERT_TRUE(found_registration->active_version());
// origin_trial_tokens must be unset.
EXPECT_FALSE(found_registration->active_version()->origin_trial_tokens());
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(scope2, &found_registration));
ASSERT_TRUE(found_registration->active_version());
// Empty origin_trial_tokens must exist.
ASSERT_TRUE(found_registration->active_version()->origin_trial_tokens());
EXPECT_TRUE(
found_registration->active_version()->origin_trial_tokens()->empty());
}
class ServiceWorkerStorageOriginTrialsDiskTest
: public ServiceWorkerStorageTest {
public:
ServiceWorkerStorageOriginTrialsDiskTest() {
blink::TrialTokenValidator::SetOriginTrialPolicyGetter(base::BindRepeating(
[](blink::OriginTrialPolicy* policy) { return policy; },
base::Unretained(&origin_trial_policy_)));
}
~ServiceWorkerStorageOriginTrialsDiskTest() override {
blink::TrialTokenValidator::ResetOriginTrialPolicyGetter();
}
void SetUp() override {
ASSERT_TRUE(InitUserDataDirectory());
ServiceWorkerStorageTest::SetUp();
}
private:
class TestOriginTrialPolicy : public blink::OriginTrialPolicy {
public:
bool IsOriginTrialsSupported() const override { return true; }
base::StringPiece GetPublicKey() const override {
return base::StringPiece(reinterpret_cast<const char*>(kTestPublicKey),
base::size(kTestPublicKey));
}
bool IsOriginSecure(const GURL& url) const override {
return content::IsOriginSecure(url);
}
};
TestOriginTrialPolicy origin_trial_policy_;
};
TEST_F(ServiceWorkerStorageOriginTrialsDiskTest, FromMainScript) {
LazyInitialize();
const GURL kScope("https://valid.example.com/scope");
const GURL kScript("https://valid.example.com/script.js");
const int64_t kRegistrationId = 1;
const int64_t kVersionId = 1;
blink::mojom::ServiceWorkerRegistrationOptions options;
options.scope = kScope;
scoped_refptr<ServiceWorkerRegistration> registration =
new ServiceWorkerRegistration(options, kRegistrationId,
context()->AsWeakPtr());
scoped_refptr<ServiceWorkerVersion> version = new ServiceWorkerVersion(
registration.get(), kScript, blink::mojom::ScriptType::kClassic,
kVersionId, context()->AsWeakPtr());
net::HttpResponseInfo http_info;
http_info.ssl_info.cert =
net::ImportCertFromFile(net::GetTestCertsDirectory(), "ok_cert.pem");
EXPECT_TRUE(http_info.ssl_info.is_valid());
// SSL3 TLS_DHE_RSA_WITH_AES_256_CBC_SHA
http_info.ssl_info.connection_status = 0x300039;
const std::string kHTTPHeaderLine("HTTP/1.1 200 OK\n\n");
const std::string kOriginTrial("Origin-Trial: ");
// Token for Feature1 which expires 2033-05-18.
// generate_token.py valid.example.com Feature1 --expire-timestamp=2000000000
// TODO(horo): Generate this sample token during the build.
const std::string kFeature1Token(
"AtiUXksymWhTv5ipBE7853JytiYb0RMj3wtEBjqu3PeufQPwV1oEaNjHt4R/oEBfcK0UiWlA"
"P2b9BE2/eThqcAYAAABYeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcGxlLmNvbTo0"
"NDMiLCAiZmVhdHVyZSI6ICJGZWF0dXJlMSIsICJleHBpcnkiOiAyMDAwMDAwMDAwfQ==");
// Token for Feature2 which expires 2033-05-18.
// generate_token.py valid.example.com Feature2 --expire-timestamp=2000000000
// TODO(horo): Generate this sample token during the build.
const std::string kFeature2Token1(
"ApmHVC6Dpez0KQNBy13o6cGuoB5AgzOLN0keQMyAN5mjebCwR0MA8/IyjKQIlyom2RuJVg/u"
"LmnqEpldfewkbA8AAABYeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcGxlLmNvbTo0"
"NDMiLCAiZmVhdHVyZSI6ICJGZWF0dXJlMiIsICJleHBpcnkiOiAyMDAwMDAwMDAwfQ==");
// Token for Feature2 which expires 2036-07-18.
// generate_token.py valid.example.com Feature2 --expire-timestamp=2100000000
// TODO(horo): Generate this sample token during the build.
const std::string kFeature2Token2(
"AmV2SSxrYstE2zSwZToy7brAbIJakd146apC/6+VDflLmc5yDfJlHGILe5+ZynlcliG7clOR"
"fHhXCzS5Lh1v4AAAAABYeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcGxlLmNvbTo0"
"NDMiLCAiZmVhdHVyZSI6ICJGZWF0dXJlMiIsICJleHBpcnkiOiAyMTAwMDAwMDAwfQ==");
// Token for Feature3 which expired 2001-09-09.
// generate_token.py valid.example.com Feature3 --expire-timestamp=1000000000
const std::string kFeature3ExpiredToken(
"AtSAc03z4qvid34W4MHMxyRFUJKlubZ+P5cs5yg6EiBWcagVbnm5uBgJMJN34pag7D5RywGV"
"ol2RFf+4Sdm1hQ4AAABYeyJvcmlnaW4iOiAiaHR0cHM6Ly92YWxpZC5leGFtcGxlLmNvbTo0"
"NDMiLCAiZmVhdHVyZSI6ICJGZWF0dXJlMyIsICJleHBpcnkiOiAxMDAwMDAwMDAwfQ==");
http_info.headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
http_info.headers->AddHeader(kOriginTrial + kFeature1Token);
http_info.headers->AddHeader(kOriginTrial + kFeature2Token1);
http_info.headers->AddHeader(kOriginTrial + kFeature2Token2);
http_info.headers->AddHeader(kOriginTrial + kFeature3ExpiredToken);
version->SetMainScriptHttpResponseInfo(http_info);
ASSERT_TRUE(version->origin_trial_tokens());
const blink::TrialTokenValidator::FeatureToTokensMap& tokens =
*version->origin_trial_tokens();
ASSERT_EQ(2UL, tokens.size());
ASSERT_EQ(1UL, tokens.at("Feature1").size());
EXPECT_EQ(kFeature1Token, tokens.at("Feature1")[0]);
ASSERT_EQ(2UL, tokens.at("Feature2").size());
EXPECT_EQ(kFeature2Token1, tokens.at("Feature2")[0]);
EXPECT_EQ(kFeature2Token2, tokens.at("Feature2")[1]);
std::vector<ResourceRecord> record;
record.push_back(ResourceRecord(1, kScript, 100));
version->script_cache_map()->SetResources(record);
version->set_fetch_handler_existence(
ServiceWorkerVersion::FetchHandlerExistence::EXISTS);
version->SetStatus(ServiceWorkerVersion::INSTALLED);
registration->SetActiveVersion(version);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(registration, version));
// Simulate browser shutdown and restart.
registration = nullptr;
version = nullptr;
InitializeTestHelper();
LazyInitialize();
scoped_refptr<ServiceWorkerRegistration> found_registration;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kScope, &found_registration));
ASSERT_TRUE(found_registration->active_version());
const blink::TrialTokenValidator::FeatureToTokensMap& found_tokens =
*found_registration->active_version()->origin_trial_tokens();
ASSERT_EQ(2UL, found_tokens.size());
ASSERT_EQ(1UL, found_tokens.at("Feature1").size());
EXPECT_EQ(kFeature1Token, found_tokens.at("Feature1")[0]);
ASSERT_EQ(2UL, found_tokens.at("Feature2").size());
EXPECT_EQ(kFeature2Token1, found_tokens.at("Feature2")[0]);
EXPECT_EQ(kFeature2Token2, found_tokens.at("Feature2")[1]);
}
// Tests loading a registration that has no navigation preload state.
TEST_F(ServiceWorkerStorageTest, AbsentNavigationPreloadState) {
const GURL origin1("http://www1.example.com");
const GURL scope1("http://www1.example.com/foo/");
RegistrationData data1;
data1.registration_id = 100;
data1.scope = scope1;
data1.script = GURL(origin1.spec() + "/script.js");
data1.version_id = 1000;
data1.is_active = true;
data1.resources_total_size_bytes = 100;
// Don't set navigation preload state to simulate old database entry.
std::vector<ResourceRecord> resources1;
resources1.push_back(ResourceRecord(1, data1.script, 100));
WriteRegistration(data1, resources1);
scoped_refptr<ServiceWorkerRegistration> found_registration;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(scope1, &found_registration));
const blink::mojom::NavigationPreloadState& registration_state =
found_registration->navigation_preload_state();
EXPECT_FALSE(registration_state.enabled);
EXPECT_EQ("true", registration_state.header);
ASSERT_TRUE(found_registration->active_version());
const blink::mojom::NavigationPreloadState& state =
found_registration->active_version()->navigation_preload_state();
EXPECT_FALSE(state.enabled);
EXPECT_EQ("true", state.header);
}
// Tests storing the script response time for DevTools.
TEST_F(ServiceWorkerStorageDiskTest, ScriptResponseTime) {
// Make a registration.
LazyInitialize();
const GURL kScope("https://example.com/scope");
const GURL kScript("https://example.com/script.js");
scoped_refptr<ServiceWorkerRegistration> registration =
CreateLiveRegistrationAndVersion(kScope, kScript);
ServiceWorkerVersion* version = registration->waiting_version();
// Give it a main script response info.
net::HttpResponseInfo http_info;
http_info.headers =
base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
http_info.response_time = base::Time::FromJsTime(19940123);
version->SetMainScriptHttpResponseInfo(http_info);
EXPECT_TRUE(version->main_script_http_info_);
EXPECT_EQ(http_info.response_time,
version->script_response_time_for_devtools_);
EXPECT_EQ(http_info.response_time, version->GetInfo().script_response_time);
// Store the registration.
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(registration, version));
// Simulate browser shutdown and restart.
registration = nullptr;
version = nullptr;
InitializeTestHelper();
LazyInitialize();
// Read the registration. The main script's response time should be gettable.
scoped_refptr<ServiceWorkerRegistration> found_registration;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kScope, &found_registration));
ASSERT_TRUE(found_registration);
auto* waiting_version = found_registration->waiting_version();
ASSERT_TRUE(waiting_version);
EXPECT_FALSE(waiting_version->main_script_http_info_);
EXPECT_EQ(http_info.response_time,
waiting_version->script_response_time_for_devtools_);
EXPECT_EQ(http_info.response_time,
waiting_version->GetInfo().script_response_time);
}
TEST_F(ServiceWorkerStorageDiskTest, RegisteredOriginCount) {
{
base::HistogramTester histogram_tester;
LazyInitialize();
EXPECT_TRUE(registered_origins().empty());
histogram_tester.ExpectUniqueSample("ServiceWorker.RegisteredOriginCount",
0, 1);
}
std::pair<GURL, GURL> scope_and_script_pairs[] = {
{GURL("https://www.example.com/scope/"),
GURL("https://www.example.com/script.js")},
{GURL("https://www.example.com/scope/foo"),
GURL("https://www.example.com/script.js")},
{GURL("https://www.test.com/scope/foobar"),
GURL("https://www.test.com/script.js")},
{GURL("https://example.com/scope/"),
GURL("https://example.com/script.js")},
};
std::vector<scoped_refptr<ServiceWorkerRegistration>> registrations;
for (const auto& pair : scope_and_script_pairs) {
registrations.emplace_back(
CreateLiveRegistrationAndVersion(pair.first, pair.second));
}
// Store all registrations.
for (const auto& registration : registrations) {
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(registration, registration->waiting_version()));
}
// Simulate browser shutdown and restart.
registrations.clear();
InitializeTestHelper();
{
base::HistogramTester histogram_tester;
LazyInitialize();
EXPECT_EQ(3UL, registered_origins().size());
histogram_tester.ExpectUniqueSample("ServiceWorker.RegisteredOriginCount",
3, 1);
}
// Re-initializing shouldn't re-record the histogram.
{
base::HistogramTester histogram_tester;
LazyInitialize();
EXPECT_EQ(3UL, registered_origins().size());
histogram_tester.ExpectTotalCount("ServiceWorker.RegisteredOriginCount", 0);
}
}
// Tests loading a registration with a disabled navigation preload
// state.
TEST_F(ServiceWorkerStorageDiskTest, DisabledNavigationPreloadState) {
LazyInitialize();
const GURL kScope("https://valid.example.com/scope");
const GURL kScript("https://valid.example.com/script.js");
scoped_refptr<ServiceWorkerRegistration> registration =
CreateLiveRegistrationAndVersion(kScope, kScript);
ServiceWorkerVersion* version = registration->waiting_version();
version->SetStatus(ServiceWorkerVersion::ACTIVATED);
registration->SetActiveVersion(version);
registration->EnableNavigationPreload(false);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(registration, version));
// Simulate browser shutdown and restart.
registration = nullptr;
version = nullptr;
InitializeTestHelper();
LazyInitialize();
scoped_refptr<ServiceWorkerRegistration> found_registration;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kScope, &found_registration));
const blink::mojom::NavigationPreloadState& registration_state =
found_registration->navigation_preload_state();
EXPECT_FALSE(registration_state.enabled);
EXPECT_EQ("true", registration_state.header);
ASSERT_TRUE(found_registration->active_version());
const blink::mojom::NavigationPreloadState& state =
found_registration->active_version()->navigation_preload_state();
EXPECT_FALSE(state.enabled);
EXPECT_EQ("true", state.header);
}
// Tests loading a registration with an enabled navigation preload state, as
// well as a custom header value.
TEST_F(ServiceWorkerStorageDiskTest, EnabledNavigationPreloadState) {
LazyInitialize();
const GURL kScope("https://valid.example.com/scope");
const GURL kScript("https://valid.example.com/script.js");
const std::string kHeaderValue("custom header value");
scoped_refptr<ServiceWorkerRegistration> registration =
CreateLiveRegistrationAndVersion(kScope, kScript);
ServiceWorkerVersion* version = registration->waiting_version();
version->SetStatus(ServiceWorkerVersion::ACTIVATED);
registration->SetActiveVersion(version);
registration->EnableNavigationPreload(true);
registration->SetNavigationPreloadHeader(kHeaderValue);
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
StoreRegistration(registration, version));
// Simulate browser shutdown and restart.
registration = nullptr;
version = nullptr;
InitializeTestHelper();
LazyInitialize();
scoped_refptr<ServiceWorkerRegistration> found_registration;
EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk,
FindRegistrationForDocument(kScope, &found_registration));
const blink::mojom::NavigationPreloadState& registration_state =
found_registration->navigation_preload_state();
EXPECT_TRUE(registration_state.enabled);
EXPECT_EQ(kHeaderValue, registration_state.header);
ASSERT_TRUE(found_registration->active_version());
const blink::mojom::NavigationPreloadState& state =
found_registration->active_version()->navigation_preload_state();
EXPECT_TRUE(state.enabled);
EXPECT_EQ(kHeaderValue, state.header);
}
} // namespace service_worker_storage_unittest
} // namespace content