blob: 010bd38629484e6e0281558a208ee8f60b5d236a [file] [log] [blame]
// Copyright 2017 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 "extensions/renderer/api_event_listeners.h"
#include "base/test/mock_callback.h"
#include "base/values.h"
#include "extensions/common/event_filter.h"
#include "extensions/renderer/api_binding_test.h"
#include "extensions/renderer/api_binding_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace extensions {
namespace {
using APIEventListenersTest = APIBindingTest;
using MockEventChangeHandler = ::testing::StrictMock<
base::MockCallback<APIEventListeners::ListenersUpdated>>;
void DoNothingOnUpdate(binding::EventListenersChanged changed,
const base::DictionaryValue* filter,
bool was_manual,
v8::Local<v8::Context> context) {}
const char kFunction[] = "(function() {})";
const char kEvent[] = "event";
} // namespace
// Test unfiltered listeners.
TEST_F(APIEventListenersTest, UnfilteredListeners) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
MockEventChangeHandler handler;
UnfilteredEventListeners listeners(handler.Get());
// Starting out, there should be no listeners.
v8::Local<v8::Function> function_a = FunctionFromString(context, kFunction);
EXPECT_EQ(0u, listeners.GetNumListeners());
EXPECT_FALSE(listeners.HasListener(function_a));
std::string error;
v8::Local<v8::Object> filter;
// Adding a new listener should trigger the callback (0 -> 1).
EXPECT_CALL(handler, Run(binding::EventListenersChanged::HAS_LISTENERS,
nullptr, true, context));
EXPECT_TRUE(listeners.AddListener(function_a, filter, context, &error));
::testing::Mock::VerifyAndClearExpectations(&handler);
// function_a should be registered as a listener, and should be returned when
// we get the listeners.
EXPECT_TRUE(listeners.HasListener(function_a));
EXPECT_EQ(1u, listeners.GetNumListeners());
EXPECT_THAT(listeners.GetListeners(nullptr, context),
testing::UnorderedElementsAre(function_a));
// Trying to add function_a again should have no effect.
EXPECT_FALSE(listeners.AddListener(function_a, filter, context, &error));
EXPECT_TRUE(listeners.HasListener(function_a));
EXPECT_EQ(1u, listeners.GetNumListeners());
v8::Local<v8::Function> function_b = FunctionFromString(context, kFunction);
// We should not yet have function_b registered, and trying to remove it
// should have no effect.
EXPECT_FALSE(listeners.HasListener(function_b));
listeners.RemoveListener(function_b, context);
EXPECT_EQ(1u, listeners.GetNumListeners());
EXPECT_THAT(listeners.GetListeners(nullptr, context),
testing::UnorderedElementsAre(function_a));
// Add function_b; there should now be two listeners, and both should be
// returned when we get the listeners. However, the callback shouldn't be
// triggered, since this isn't a 0 -> 1 or 1 -> 0 transition.
EXPECT_TRUE(listeners.AddListener(function_b, filter, context, &error));
EXPECT_TRUE(listeners.HasListener(function_b));
EXPECT_EQ(2u, listeners.GetNumListeners());
EXPECT_THAT(listeners.GetListeners(nullptr, context),
testing::UnorderedElementsAre(function_a, function_b));
// Remove function_a; there should now be only one listener. The callback
// shouldn't be triggered.
listeners.RemoveListener(function_a, context);
EXPECT_FALSE(listeners.HasListener(function_a));
EXPECT_EQ(1u, listeners.GetNumListeners());
EXPECT_THAT(listeners.GetListeners(nullptr, context),
testing::UnorderedElementsAre(function_b));
// Remove function_b (the final listener). No more listeners should remain.
EXPECT_CALL(handler, Run(binding::EventListenersChanged::NO_LISTENERS,
nullptr, true, context));
listeners.RemoveListener(function_b, context);
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_FALSE(listeners.HasListener(function_b));
EXPECT_EQ(0u, listeners.GetNumListeners());
EXPECT_TRUE(listeners.GetListeners(nullptr, context).empty());
}
// Tests the invalidation of unfiltered listeners.
TEST_F(APIEventListenersTest, UnfilteredListenersInvalidation) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
MockEventChangeHandler handler;
UnfilteredEventListeners listeners(handler.Get());
listeners.Invalidate(context);
v8::Local<v8::Function> function_a = FunctionFromString(context, kFunction);
v8::Local<v8::Function> function_b = FunctionFromString(context, kFunction);
std::string error;
v8::Local<v8::Object> filter;
EXPECT_CALL(handler, Run(binding::EventListenersChanged::HAS_LISTENERS,
nullptr, true, context));
EXPECT_TRUE(listeners.AddListener(function_a, filter, context, &error));
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_TRUE(listeners.AddListener(function_b, filter, context, &error));
EXPECT_CALL(handler, Run(binding::EventListenersChanged::NO_LISTENERS,
nullptr, false, context));
listeners.Invalidate(context);
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_EQ(0u, listeners.GetNumListeners());
}
// Tests that unfiltered listeners ignore the filtering info.
TEST_F(APIEventListenersTest, UnfilteredListenersIgnoreFilteringInfo) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
UnfilteredEventListeners listeners(base::Bind(&DoNothingOnUpdate));
v8::Local<v8::Function> function = FunctionFromString(context, kFunction);
std::string error;
v8::Local<v8::Object> filter;
EXPECT_TRUE(listeners.AddListener(function, filter, context, &error));
std::unique_ptr<base::DictionaryValue> filtering_info_dict =
DictionaryValueFromString("{'url': 'http://example.com/foo'}");
EventFilteringInfo filtering_info(*filtering_info_dict);
EXPECT_THAT(listeners.GetListeners(&filtering_info, context),
testing::UnorderedElementsAre(function));
}
// Tests filtered listeners.
TEST_F(APIEventListenersTest, FilteredListeners) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
MockEventChangeHandler handler;
EventFilter event_filter;
FilteredEventListeners listeners(handler.Get(), kEvent, &event_filter);
// Starting out, there should be no listeners registered.
v8::Local<v8::Function> function_a = FunctionFromString(context, kFunction);
EXPECT_EQ(0u, listeners.GetNumListeners());
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kEvent));
EXPECT_FALSE(listeners.HasListener(function_a));
v8::Local<v8::Object> empty_filter;
std::string error;
// Register function_a with no filter; this is equivalent to registering for
// all events. The callback should be triggered since this is a 0 -> 1
// transition.
// Note that we don't test the passed filter here. This is mostly because it's
// a pain to match against a DictionaryValue (which doesn't have an
// operator==).
EXPECT_CALL(handler, Run(binding::EventListenersChanged::HAS_LISTENERS,
testing::NotNull(), true, context));
EXPECT_TRUE(listeners.AddListener(function_a, empty_filter, context, &error));
::testing::Mock::VerifyAndClearExpectations(&handler);
// function_a should be registered, and should be returned when we get the
// listeners.
EXPECT_TRUE(listeners.HasListener(function_a));
EXPECT_EQ(1u, listeners.GetNumListeners());
EXPECT_THAT(listeners.GetListeners(nullptr, context),
testing::UnorderedElementsAre(function_a));
// It should also be registered in the event filter.
EXPECT_EQ(1, event_filter.GetMatcherCountForEventForTesting(kEvent));
// Since function_a has no filter, associating a specific url should still
// return function_a.
std::unique_ptr<base::DictionaryValue> filtering_info_match_dict =
DictionaryValueFromString("{'url': 'http://example.com/foo'}");
ASSERT_TRUE(filtering_info_match_dict);
EventFilteringInfo filtering_info_match(*filtering_info_match_dict);
EXPECT_THAT(listeners.GetListeners(&filtering_info_match, context),
testing::UnorderedElementsAre(function_a));
// Trying to add function_a again should have no effect.
EXPECT_FALSE(
listeners.AddListener(function_a, empty_filter, context, &error));
EXPECT_TRUE(listeners.HasListener(function_a));
EXPECT_EQ(1u, listeners.GetNumListeners());
v8::Local<v8::Function> function_b = FunctionFromString(context, kFunction);
// function_b should not yet be registered, and trying to remove it should
// have no effect.
EXPECT_FALSE(listeners.HasListener(function_b));
listeners.RemoveListener(function_b, context);
EXPECT_EQ(1u, listeners.GetNumListeners());
EXPECT_THAT(listeners.GetListeners(nullptr, context),
testing::UnorderedElementsAre(function_a));
// Register function_b with a filter for pathContains: 'foo'. Unlike
// unfiltered listeners, this *should* trigger the callback, since there is
// no other listener registered with this same filter.
v8::Local<v8::Object> path_filter;
{
v8::Local<v8::Value> val =
V8ValueFromScriptSource(context, "({url: [{pathContains: 'foo'}]})");
ASSERT_TRUE(val->IsObject());
path_filter = val.As<v8::Object>();
}
EXPECT_CALL(handler, Run(binding::EventListenersChanged::HAS_LISTENERS,
testing::NotNull(), true, context));
EXPECT_TRUE(listeners.AddListener(function_b, path_filter, context, &error));
::testing::Mock::VerifyAndClearExpectations(&handler);
// function_b should be present.
EXPECT_TRUE(listeners.HasListener(function_b));
EXPECT_EQ(2u, listeners.GetNumListeners());
EXPECT_EQ(2, event_filter.GetMatcherCountForEventForTesting(kEvent));
// function_b should ignore calls that don't specify an url, since they, by
// definition, don't match.
EXPECT_THAT(listeners.GetListeners(nullptr, context),
testing::UnorderedElementsAre(function_a));
// function_b should be included for matching urls...
EXPECT_THAT(listeners.GetListeners(&filtering_info_match, context),
testing::UnorderedElementsAre(function_a, function_b));
// ... but not urls that don't match.
std::unique_ptr<base::DictionaryValue> filtering_info_no_match_dict =
DictionaryValueFromString("{'url': 'http://example.com/bar'}");
ASSERT_TRUE(filtering_info_no_match_dict);
EventFilteringInfo filtering_info_no_match(*filtering_info_no_match_dict);
EXPECT_THAT(listeners.GetListeners(&filtering_info_no_match, context),
testing::UnorderedElementsAre(function_a));
// Remove function_a. Since filtered listeners notify whenever there's a
// change in listeners registered with a specific filter, this should trigger
// the callback.
EXPECT_CALL(handler, Run(binding::EventListenersChanged::NO_LISTENERS,
testing::NotNull(), true, context));
listeners.RemoveListener(function_a, context);
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_FALSE(listeners.HasListener(function_a));
EXPECT_EQ(1u, listeners.GetNumListeners());
EXPECT_EQ(1, event_filter.GetMatcherCountForEventForTesting(kEvent));
// function_b should be the only listener remaining, so we shouldn't find
// any listeners for events without matching filters.
EXPECT_TRUE(listeners.GetListeners(nullptr, context).empty());
EXPECT_THAT(listeners.GetListeners(&filtering_info_match, context),
testing::UnorderedElementsAre(function_b));
EXPECT_TRUE(
listeners.GetListeners(&filtering_info_no_match, context).empty());
// Remove function_b. No listeners should remain.
EXPECT_CALL(handler, Run(binding::EventListenersChanged::NO_LISTENERS,
testing::NotNull(), true, context));
listeners.RemoveListener(function_b, context);
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_FALSE(listeners.HasListener(function_b));
EXPECT_EQ(0u, listeners.GetNumListeners());
EXPECT_TRUE(listeners.GetListeners(nullptr, context).empty());
EXPECT_TRUE(listeners.GetListeners(&filtering_info_match, context).empty());
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kEvent));
}
// Tests that adding multiple listeners with the same filter doesn't trigger
// the update callback.
TEST_F(APIEventListenersTest,
UnfilteredListenersWithSameFilterDontTriggerUpdate) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
MockEventChangeHandler handler;
EventFilter event_filter;
FilteredEventListeners listeners(handler.Get(), kEvent, &event_filter);
auto get_filter = [context]() {
return V8ValueFromScriptSource(context, "({url: [{pathContains: 'foo'}]})")
.As<v8::Object>();
};
v8::Local<v8::Function> function_a = FunctionFromString(context, kFunction);
std::string error;
EXPECT_CALL(handler, Run(binding::EventListenersChanged::HAS_LISTENERS,
testing::NotNull(), true, context));
EXPECT_TRUE(listeners.AddListener(function_a, get_filter(), context, &error));
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_EQ(1, event_filter.GetMatcherCountForEventForTesting(kEvent));
v8::Local<v8::Function> function_b = FunctionFromString(context, kFunction);
v8::Local<v8::Function> function_c = FunctionFromString(context, kFunction);
EXPECT_TRUE(listeners.AddListener(function_b, get_filter(), context, &error));
EXPECT_TRUE(listeners.AddListener(function_c, get_filter(), context, &error));
EXPECT_EQ(3u, listeners.GetNumListeners());
EXPECT_EQ(3, event_filter.GetMatcherCountForEventForTesting(kEvent));
std::unique_ptr<base::DictionaryValue> filtering_info_match_dict =
DictionaryValueFromString("{'url': 'http://example.com/foo'}");
ASSERT_TRUE(filtering_info_match_dict);
EventFilteringInfo filtering_info_match(*filtering_info_match_dict);
EXPECT_THAT(
listeners.GetListeners(&filtering_info_match, context),
testing::UnorderedElementsAre(function_a, function_b, function_c));
listeners.RemoveListener(function_c, context);
listeners.RemoveListener(function_b, context);
EXPECT_CALL(handler, Run(binding::EventListenersChanged::NO_LISTENERS,
testing::NotNull(), true, context));
listeners.RemoveListener(function_a, context);
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kEvent));
}
// Tests that trying to add a listener with an invalid filter fails.
TEST_F(APIEventListenersTest, UnfilteredListenersError) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
EventFilter event_filter;
FilteredEventListeners listeners(base::Bind(&DoNothingOnUpdate), kEvent,
&event_filter);
v8::Local<v8::Object> invalid_filter =
V8ValueFromScriptSource(context, "({url: 'some string'})")
.As<v8::Object>();
v8::Local<v8::Function> function = FunctionFromString(context, kFunction);
std::string error;
EXPECT_FALSE(
listeners.AddListener(function, invalid_filter, context, &error));
EXPECT_FALSE(error.empty());
}
// Tests that adding listeners for multiple different events is correctly
// recorded in the EventFilter.
TEST_F(APIEventListenersTest, MultipleUnfilteredListenerEvents) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
const char kAlpha[] = "alpha";
const char kBeta[] = "beta";
EventFilter event_filter;
FilteredEventListeners listeners_a(base::Bind(&DoNothingOnUpdate), kAlpha,
&event_filter);
FilteredEventListeners listeners_b(base::Bind(&DoNothingOnUpdate), kBeta,
&event_filter);
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kAlpha));
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kBeta));
std::string error;
v8::Local<v8::Object> filter;
v8::Local<v8::Function> function_a = FunctionFromString(context, kFunction);
EXPECT_TRUE(listeners_a.AddListener(function_a, filter, context, &error));
EXPECT_EQ(1, event_filter.GetMatcherCountForEventForTesting(kAlpha));
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kBeta));
v8::Local<v8::Function> function_b = FunctionFromString(context, kFunction);
EXPECT_TRUE(listeners_b.AddListener(function_b, filter, context, &error));
EXPECT_EQ(1, event_filter.GetMatcherCountForEventForTesting(kAlpha));
EXPECT_EQ(1, event_filter.GetMatcherCountForEventForTesting(kBeta));
listeners_b.RemoveListener(function_b, context);
EXPECT_EQ(1, event_filter.GetMatcherCountForEventForTesting(kAlpha));
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kBeta));
listeners_a.RemoveListener(function_a, context);
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kAlpha));
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kBeta));
}
// Tests the invalidation of filtered listeners.
TEST_F(APIEventListenersTest, FilteredListenersInvalidation) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
MockEventChangeHandler handler;
EventFilter event_filter;
FilteredEventListeners listeners(handler.Get(), kEvent, &event_filter);
listeners.Invalidate(context);
v8::Local<v8::Object> empty_filter;
v8::Local<v8::Object> filter =
V8ValueFromScriptSource(context, "({url: [{pathContains: 'foo'}]})")
.As<v8::Object>();
std::string error;
v8::Local<v8::Function> function_a = FunctionFromString(context, kFunction);
v8::Local<v8::Function> function_b = FunctionFromString(context, kFunction);
v8::Local<v8::Function> function_c = FunctionFromString(context, kFunction);
EXPECT_CALL(handler, Run(binding::EventListenersChanged::HAS_LISTENERS,
testing::NotNull(), true, context));
EXPECT_TRUE(listeners.AddListener(function_a, empty_filter, context, &error));
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_CALL(handler, Run(binding::EventListenersChanged::HAS_LISTENERS,
testing::NotNull(), true, context));
EXPECT_TRUE(listeners.AddListener(function_b, filter, context, &error));
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_TRUE(listeners.AddListener(function_c, filter, context, &error));
// Since two listener filters are present in the list, we should be notified
// of each going away when we invalidate the context.
EXPECT_CALL(handler, Run(binding::EventListenersChanged::NO_LISTENERS,
testing::NotNull(), false, context))
.Times(2);
listeners.Invalidate(context);
::testing::Mock::VerifyAndClearExpectations(&handler);
EXPECT_EQ(0u, listeners.GetNumListeners());
EXPECT_EQ(0, event_filter.GetMatcherCountForEventForTesting(kEvent));
}
} // namespace extensions