blob: 1fac9649dc542750a42ab584eafec9f74d726d1e [file] [log] [blame]
// Copyright 2014 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 "modules/serviceworkers/ServiceWorkerContainer.h"
#include <memory>
#include <utility>
#include "bindings/core/v8/Dictionary.h"
#include "bindings/core/v8/ScriptFunction.h"
#include "bindings/core/v8/ScriptPromise.h"
#include "bindings/core/v8/V8BindingForCore.h"
#include "bindings/core/v8/V8DOMException.h"
#include "bindings/core/v8/V8GCController.h"
#include "core/dom/DOMException.h"
#include "core/dom/Document.h"
#include "core/dom/ExecutionContext.h"
#include "core/page/FocusController.h"
#include "core/testing/DummyPageHolder.h"
#include "modules/serviceworkers/NavigatorServiceWorker.h"
#include "modules/serviceworkers/ServiceWorkerContainerClient.h"
#include "platform/bindings/ScriptState.h"
#include "platform/weborigin/KURL.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "platform/wtf/PtrUtil.h"
#include "platform/wtf/text/WTFString.h"
#include "public/platform/WebURL.h"
#include "public/platform/modules/serviceworker/WebServiceWorkerClientsInfo.h"
#include "public/platform/modules/serviceworker/WebServiceWorkerProvider.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
// Promise-related test support.
struct StubScriptFunction {
public:
StubScriptFunction() : call_count_(0) {}
// The returned ScriptFunction can outlive the StubScriptFunction,
// but it should not be called after the StubScriptFunction dies.
v8::Local<v8::Function> GetFunction(ScriptState* script_state) {
return ScriptFunctionImpl::CreateFunction(script_state, *this);
}
size_t CallCount() { return call_count_; }
ScriptValue Arg() { return arg_; }
private:
size_t call_count_;
ScriptValue arg_;
class ScriptFunctionImpl : public ScriptFunction {
public:
static v8::Local<v8::Function> CreateFunction(ScriptState* script_state,
StubScriptFunction& owner) {
ScriptFunctionImpl* self = new ScriptFunctionImpl(script_state, owner);
return self->BindToV8Function();
}
private:
ScriptFunctionImpl(ScriptState* script_state, StubScriptFunction& owner)
: ScriptFunction(script_state), owner_(owner) {}
ScriptValue Call(ScriptValue arg) override {
owner_.arg_ = arg;
owner_.call_count_++;
return ScriptValue();
}
StubScriptFunction& owner_;
};
};
class ScriptValueTest {
public:
virtual ~ScriptValueTest() {}
virtual void operator()(ScriptValue) const = 0;
};
// Runs microtasks and expects |promise| to be rejected. Calls
// |valueTest| with the value passed to |reject|, if any.
void ExpectRejected(ScriptState* script_state,
ScriptPromise& promise,
const ScriptValueTest& value_test) {
StubScriptFunction resolved, rejected;
promise.Then(resolved.GetFunction(script_state),
rejected.GetFunction(script_state));
v8::MicrotasksScope::PerformCheckpoint(promise.GetIsolate());
EXPECT_EQ(0ul, resolved.CallCount());
EXPECT_EQ(1ul, rejected.CallCount());
if (rejected.CallCount())
value_test(rejected.Arg());
}
// DOM-related test support.
// Matches a ScriptValue and a DOMException with a specific name and message.
class ExpectDOMException : public ScriptValueTest {
public:
ExpectDOMException(const String& expected_name,
const String& expected_message)
: expected_name_(expected_name), expected_message_(expected_message) {}
~ExpectDOMException() override {}
void operator()(ScriptValue value) const override {
DOMException* exception = V8DOMException::ToImplWithTypeCheck(
value.GetIsolate(), value.V8Value());
EXPECT_TRUE(exception) << "the value should be a DOMException";
if (!exception)
return;
EXPECT_EQ(expected_name_, exception->name());
EXPECT_EQ(expected_message_, exception->message());
}
private:
String expected_name_;
String expected_message_;
};
// Service Worker-specific tests.
class NotReachedWebServiceWorkerProvider : public WebServiceWorkerProvider {
public:
~NotReachedWebServiceWorkerProvider() override {}
void RegisterServiceWorker(
const WebURL& pattern,
const WebURL& script_url,
std::unique_ptr<WebServiceWorkerRegistrationCallbacks> callbacks)
override {
ADD_FAILURE()
<< "the provider should not be called to register a Service Worker";
}
bool ValidateScopeAndScriptURL(const WebURL& scope,
const WebURL& script_url,
WebString* error_message) override {
return true;
}
};
class ServiceWorkerContainerTest : public ::testing::Test {
protected:
ServiceWorkerContainerTest() : page_(DummyPageHolder::Create()) {}
~ServiceWorkerContainerTest() override {
page_.reset();
V8GCController::CollectAllGarbageForTesting(GetIsolate());
}
ExecutionContext* GetExecutionContext() { return &(page_->GetDocument()); }
NavigatorServiceWorker* GetNavigatorServiceWorker() {
return NavigatorServiceWorker::From(page_->GetDocument());
}
v8::Isolate* GetIsolate() { return v8::Isolate::GetCurrent(); }
ScriptState* GetScriptState() {
return ToScriptStateForMainWorld(page_->GetDocument().GetFrame());
}
void Provide(std::unique_ptr<WebServiceWorkerProvider> provider) {
Supplement<Document>::ProvideTo(
page_->GetDocument(), ServiceWorkerContainerClient::SupplementName(),
new ServiceWorkerContainerClient(page_->GetDocument(),
std::move(provider)));
}
void SetPageURL(const String& url) {
// For URL completion.
page_->GetDocument().SetURL(KURL(NullURL(), url));
// The basis for security checks.
page_->GetDocument().SetSecurityOrigin(
SecurityOrigin::CreateFromString(url));
}
void TestRegisterRejected(const String& script_url,
const String& scope,
const ScriptValueTest& value_test) {
// When the registration is rejected, a register call must not reach
// the provider.
Provide(std::make_unique<NotReachedWebServiceWorkerProvider>());
ServiceWorkerContainer* container = ServiceWorkerContainer::Create(
GetExecutionContext(), GetNavigatorServiceWorker());
ScriptState::Scope script_scope(GetScriptState());
RegistrationOptions options;
options.setScope(scope);
ScriptPromise promise =
container->registerServiceWorker(GetScriptState(), script_url, options);
ExpectRejected(GetScriptState(), promise, value_test);
}
void TestGetRegistrationRejected(const String& document_url,
const ScriptValueTest& value_test) {
Provide(std::make_unique<NotReachedWebServiceWorkerProvider>());
ServiceWorkerContainer* container = ServiceWorkerContainer::Create(
GetExecutionContext(), GetNavigatorServiceWorker());
ScriptState::Scope script_scope(GetScriptState());
ScriptPromise promise =
container->getRegistration(GetScriptState(), document_url);
ExpectRejected(GetScriptState(), promise, value_test);
}
private:
std::unique_ptr<DummyPageHolder> page_;
};
TEST_F(ServiceWorkerContainerTest, Register_NonSecureOriginIsRejected) {
SetPageURL("http://www.example.com/");
TestRegisterRejected(
"http://www.example.com/worker.js", "http://www.example.com/",
ExpectDOMException(
"SecurityError",
"Only secure origins are allowed (see: https://goo.gl/Y0ZkNV)."));
}
TEST_F(ServiceWorkerContainerTest, Register_CrossOriginScriptIsRejected) {
SetPageURL("https://www.example.com");
TestRegisterRejected(
"https://www.example.com:8080/", // Differs by port
"https://www.example.com/",
ExpectDOMException("SecurityError",
"Failed to register a ServiceWorker: The origin of "
"the provided scriptURL "
"('https://www.example.com:8080') does not match the "
"current origin ('https://www.example.com')."));
}
TEST_F(ServiceWorkerContainerTest, Register_CrossOriginScopeIsRejected) {
SetPageURL("https://www.example.com");
TestRegisterRejected(
"https://www.example.com",
"wss://www.example.com/", // Differs by protocol
ExpectDOMException("SecurityError",
"Failed to register a ServiceWorker: The origin of "
"the provided scope ('wss://www.example.com') does "
"not match the current origin "
"('https://www.example.com')."));
}
TEST_F(ServiceWorkerContainerTest, GetRegistration_NonSecureOriginIsRejected) {
SetPageURL("http://www.example.com/");
TestGetRegistrationRejected(
"http://www.example.com/",
ExpectDOMException(
"SecurityError",
"Only secure origins are allowed (see: https://goo.gl/Y0ZkNV)."));
}
TEST_F(ServiceWorkerContainerTest, GetRegistration_CrossOriginURLIsRejected) {
SetPageURL("https://www.example.com/");
TestGetRegistrationRejected(
"https://foo.example.com/", // Differs by host
ExpectDOMException("SecurityError",
"Failed to get a ServiceWorkerRegistration: The "
"origin of the provided documentURL "
"('https://foo.example.com') does not match the "
"current origin ('https://www.example.com')."));
}
class StubWebServiceWorkerProvider {
public:
StubWebServiceWorkerProvider()
: register_call_count_(0),
get_registration_call_count_(0),
update_via_cache_(mojom::ServiceWorkerUpdateViaCache::kImports) {}
// Creates a WebServiceWorkerProvider. This can outlive the
// StubWebServiceWorkerProvider, but |registerServiceWorker| and
// other methods must not be called after the
// StubWebServiceWorkerProvider dies.
std::unique_ptr<WebServiceWorkerProvider> Provider() {
return WTF::WrapUnique(new WebServiceWorkerProviderImpl(*this));
}
size_t RegisterCallCount() { return register_call_count_; }
const WebURL& RegisterScope() { return register_scope_; }
const WebURL& RegisterScriptURL() { return register_script_url_; }
size_t GetRegistrationCallCount() { return get_registration_call_count_; }
const WebURL& GetRegistrationURL() { return get_registration_url_; }
mojom::ServiceWorkerUpdateViaCache UpdateViaCache() const {
return update_via_cache_;
}
private:
class WebServiceWorkerProviderImpl : public WebServiceWorkerProvider {
public:
WebServiceWorkerProviderImpl(StubWebServiceWorkerProvider& owner)
: owner_(owner) {}
~WebServiceWorkerProviderImpl() override {}
void RegisterServiceWorker(
const WebURL& pattern,
const WebURL& script_url,
std::unique_ptr<WebServiceWorkerRegistrationCallbacks> callbacks)
override {
owner_.register_call_count_++;
owner_.register_scope_ = pattern;
owner_.register_script_url_ = script_url;
registration_callbacks_to_delete_.push_back(std::move(callbacks));
}
void GetRegistration(
const WebURL& document_url,
std::unique_ptr<WebServiceWorkerGetRegistrationCallbacks> callbacks)
override {
owner_.get_registration_call_count_++;
owner_.get_registration_url_ = document_url;
get_registration_callbacks_to_delete_.push_back(std::move(callbacks));
}
bool ValidateScopeAndScriptURL(const WebURL& scope,
const WebURL& script_url,
WebString* error_message) override {
return true;
}
private:
StubWebServiceWorkerProvider& owner_;
Vector<std::unique_ptr<WebServiceWorkerRegistrationCallbacks>>
registration_callbacks_to_delete_;
Vector<std::unique_ptr<WebServiceWorkerGetRegistrationCallbacks>>
get_registration_callbacks_to_delete_;
};
private:
size_t register_call_count_;
WebURL register_scope_;
WebURL register_script_url_;
size_t get_registration_call_count_;
WebURL get_registration_url_;
mojom::ServiceWorkerUpdateViaCache update_via_cache_;
};
TEST_F(ServiceWorkerContainerTest,
RegisterUnregister_NonHttpsSecureOriginDelegatesToProvider) {
SetPageURL("http://localhost/x/index.html");
StubWebServiceWorkerProvider stub_provider;
Provide(stub_provider.Provider());
ServiceWorkerContainer* container = ServiceWorkerContainer::Create(
GetExecutionContext(), GetNavigatorServiceWorker());
// register
{
ScriptState::Scope script_scope(GetScriptState());
RegistrationOptions options;
options.setScope("y/");
container->registerServiceWorker(GetScriptState(), "/x/y/worker.js",
options);
EXPECT_EQ(1ul, stub_provider.RegisterCallCount());
EXPECT_EQ(WebURL(KURL(NullURL(), "http://localhost/x/y/")),
stub_provider.RegisterScope());
EXPECT_EQ(WebURL(KURL(NullURL(), "http://localhost/x/y/worker.js")),
stub_provider.RegisterScriptURL());
EXPECT_EQ(mojom::ServiceWorkerUpdateViaCache::kImports,
stub_provider.UpdateViaCache());
}
}
TEST_F(ServiceWorkerContainerTest,
GetRegistration_OmittedDocumentURLDefaultsToPageURL) {
SetPageURL("http://localhost/x/index.html");
StubWebServiceWorkerProvider stub_provider;
Provide(stub_provider.Provider());
ServiceWorkerContainer* container = ServiceWorkerContainer::Create(
GetExecutionContext(), GetNavigatorServiceWorker());
{
ScriptState::Scope script_scope(GetScriptState());
container->getRegistration(GetScriptState(), "");
EXPECT_EQ(1ul, stub_provider.GetRegistrationCallCount());
EXPECT_EQ(WebURL(KURL(NullURL(), "http://localhost/x/index.html")),
stub_provider.GetRegistrationURL());
EXPECT_EQ(mojom::ServiceWorkerUpdateViaCache::kImports,
stub_provider.UpdateViaCache());
}
}
} // namespace
} // namespace blink