// Copyright (c) 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 "extensions/browser/api/declarative/declarative_rule.h" #include "base/bind.h" #include "base/message_loop/message_loop.h" #include "base/test/values_test_util.h" #include "base/values.h" #include "components/url_matcher/url_matcher_constants.h" #include "extensions/common/extension_builder.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" using base::test::ParseJson; using url_matcher::URLMatcher; using url_matcher::URLMatcherConditionFactory; using url_matcher::URLMatcherConditionSet; namespace extensions { namespace { template linked_ptr ScopedToLinkedPtr(scoped_ptr ptr) { return linked_ptr(ptr.release()); } scoped_ptr SimpleManifest() { return DictionaryBuilder() .Set("name", "extension") .Set("manifest_version", 2) .Set("version", "1.0") .Build(); } } // namespace struct RecordingCondition { typedef int MatchData; URLMatcherConditionFactory* factory; scoped_ptr value; void GetURLMatcherConditionSets( URLMatcherConditionSet::Vector* condition_sets) const { // No condition sets. } static scoped_ptr Create( const Extension* extension, URLMatcherConditionFactory* url_matcher_condition_factory, const base::Value& condition, std::string* error) { const base::DictionaryValue* dict = NULL; if (condition.GetAsDictionary(&dict) && dict->HasKey("bad_key")) { *error = "Found error key"; return scoped_ptr(); } scoped_ptr result(new RecordingCondition()); result->factory = url_matcher_condition_factory; result->value.reset(condition.DeepCopy()); return result.Pass(); } }; typedef DeclarativeConditionSet RecordingConditionSet; TEST(DeclarativeConditionTest, ErrorConditionSet) { URLMatcher matcher; RecordingConditionSet::AnyVector conditions; conditions.push_back(ScopedToLinkedPtr(ParseJson("{\"key\": 1}"))); conditions.push_back(ScopedToLinkedPtr(ParseJson("{\"bad_key\": 2}"))); std::string error; scoped_ptr result = RecordingConditionSet::Create( NULL, matcher.condition_factory(), conditions, &error); EXPECT_EQ("Found error key", error); ASSERT_FALSE(result); } TEST(DeclarativeConditionTest, CreateConditionSet) { URLMatcher matcher; RecordingConditionSet::AnyVector conditions; conditions.push_back(ScopedToLinkedPtr(ParseJson("{\"key\": 1}"))); conditions.push_back(ScopedToLinkedPtr(ParseJson("[\"val1\", 2]"))); // Test insertion std::string error; scoped_ptr result = RecordingConditionSet::Create( NULL, matcher.condition_factory(), conditions, &error); EXPECT_EQ("", error); ASSERT_TRUE(result); EXPECT_EQ(2u, result->conditions().size()); EXPECT_EQ(matcher.condition_factory(), result->conditions()[0]->factory); EXPECT_TRUE(ParseJson("{\"key\": 1}")->Equals( result->conditions()[0]->value.get())); } struct FulfillableCondition { struct MatchData { int value; const std::set& url_matches; }; scoped_refptr condition_set; int condition_set_id; int max_value; URLMatcherConditionSet::ID url_matcher_condition_set_id() const { return condition_set_id; } scoped_refptr url_matcher_condition_set() const { return condition_set; } void GetURLMatcherConditionSets( URLMatcherConditionSet::Vector* condition_sets) const { if (condition_set.get()) condition_sets->push_back(condition_set); } bool IsFulfilled(const MatchData& match_data) const { if (condition_set_id != -1 && !ContainsKey(match_data.url_matches, condition_set_id)) return false; return match_data.value <= max_value; } static scoped_ptr Create( const Extension* extension, URLMatcherConditionFactory* url_matcher_condition_factory, const base::Value& condition, std::string* error) { scoped_ptr result(new FulfillableCondition()); const base::DictionaryValue* dict; if (!condition.GetAsDictionary(&dict)) { *error = "Expected dict"; return result.Pass(); } if (!dict->GetInteger("url_id", &result->condition_set_id)) result->condition_set_id = -1; if (!dict->GetInteger("max", &result->max_value)) *error = "Expected integer at ['max']"; if (result->condition_set_id != -1) { result->condition_set = new URLMatcherConditionSet( result->condition_set_id, URLMatcherConditionSet::Conditions()); } return result.Pass(); } }; TEST(DeclarativeConditionTest, FulfillConditionSet) { typedef DeclarativeConditionSet FulfillableConditionSet; FulfillableConditionSet::AnyVector conditions; conditions.push_back(ScopedToLinkedPtr(ParseJson( "{\"url_id\": 1, \"max\": 3}"))); conditions.push_back(ScopedToLinkedPtr(ParseJson( "{\"url_id\": 2, \"max\": 5}"))); conditions.push_back(ScopedToLinkedPtr(ParseJson( "{\"url_id\": 3, \"max\": 1}"))); conditions.push_back(ScopedToLinkedPtr(ParseJson( "{\"max\": -5}"))); // No url. // Test insertion std::string error; scoped_ptr result = FulfillableConditionSet::Create(NULL, NULL, conditions, &error); ASSERT_EQ("", error); ASSERT_TRUE(result); EXPECT_EQ(4u, result->conditions().size()); std::set url_matches; FulfillableCondition::MatchData match_data = { 0, url_matches }; EXPECT_FALSE(result->IsFulfilled(1, match_data)) << "Testing an ID that's not in url_matches forwards to the Condition, " << "which doesn't match."; EXPECT_FALSE(result->IsFulfilled(-1, match_data)) << "Testing the 'no ID' value tries to match the 4th condition, but " << "its max is too low."; match_data.value = -5; EXPECT_TRUE(result->IsFulfilled(-1, match_data)) << "Testing the 'no ID' value tries to match the 4th condition, and " << "this value is low enough."; url_matches.insert(1); match_data.value = 3; EXPECT_TRUE(result->IsFulfilled(1, match_data)) << "Tests a condition with a url matcher, for a matching value."; match_data.value = 4; EXPECT_FALSE(result->IsFulfilled(1, match_data)) << "Tests a condition with a url matcher, for a non-matching value " << "that would match a different condition."; url_matches.insert(2); EXPECT_TRUE(result->IsFulfilled(2, match_data)) << "Tests with 2 elements in the match set."; // Check the condition sets: URLMatcherConditionSet::Vector condition_sets; result->GetURLMatcherConditionSets(&condition_sets); ASSERT_EQ(3U, condition_sets.size()); EXPECT_EQ(1, condition_sets[0]->id()); EXPECT_EQ(2, condition_sets[1]->id()); EXPECT_EQ(3, condition_sets[2]->id()); } // DeclarativeAction class SummingAction : public base::RefCounted { public: typedef int ApplyInfo; SummingAction(int increment, int min_priority) : increment_(increment), min_priority_(min_priority) {} static scoped_refptr Create( content::BrowserContext* browser_context, const Extension* extension, const base::Value& action, std::string* error, bool* bad_message) { int increment = 0; int min_priority = 0; const base::DictionaryValue* dict = NULL; EXPECT_TRUE(action.GetAsDictionary(&dict)); if (dict->HasKey("error")) { EXPECT_TRUE(dict->GetString("error", error)); return scoped_refptr(NULL); } if (dict->HasKey("bad")) { *bad_message = true; return scoped_refptr(NULL); } EXPECT_TRUE(dict->GetInteger("value", &increment)); dict->GetInteger("priority", &min_priority); return scoped_refptr( new SummingAction(increment, min_priority)); } void Apply(const std::string& extension_id, const base::Time& install_time, int* sum) const { *sum += increment_; } int increment() const { return increment_; } int minimum_priority() const { return min_priority_; } private: friend class base::RefCounted; virtual ~SummingAction() {} int increment_; int min_priority_; }; typedef DeclarativeActionSet SummingActionSet; TEST(DeclarativeActionTest, ErrorActionSet) { SummingActionSet::AnyVector actions; actions.push_back(ScopedToLinkedPtr(ParseJson("{\"value\": 1}"))); actions.push_back(ScopedToLinkedPtr(ParseJson("{\"error\": \"the error\"}"))); std::string error; bool bad = false; scoped_ptr result = SummingActionSet::Create(NULL, NULL, actions, &error, &bad); EXPECT_EQ("the error", error); EXPECT_FALSE(bad); EXPECT_FALSE(result); actions.clear(); actions.push_back(ScopedToLinkedPtr(ParseJson("{\"value\": 1}"))); actions.push_back(ScopedToLinkedPtr(ParseJson("{\"bad\": 3}"))); result = SummingActionSet::Create(NULL, NULL, actions, &error, &bad); EXPECT_EQ("", error); EXPECT_TRUE(bad); EXPECT_FALSE(result); } TEST(DeclarativeActionTest, ApplyActionSet) { SummingActionSet::AnyVector actions; actions.push_back(ScopedToLinkedPtr(ParseJson( "{\"value\": 1," " \"priority\": 5}"))); actions.push_back(ScopedToLinkedPtr(ParseJson("{\"value\": 2}"))); // Test insertion std::string error; bool bad = false; scoped_ptr result = SummingActionSet::Create(NULL, NULL, actions, &error, &bad); EXPECT_EQ("", error); EXPECT_FALSE(bad); ASSERT_TRUE(result); EXPECT_EQ(2u, result->actions().size()); int sum = 0; result->Apply("ext_id", base::Time(), &sum); EXPECT_EQ(3, sum); EXPECT_EQ(5, result->GetMinimumPriority()); } TEST(DeclarativeRuleTest, Create) { typedef DeclarativeRule Rule; linked_ptr json_rule(new Rule::JsonRule); ASSERT_TRUE(Rule::JsonRule::Populate( *ParseJson("{ \n" " \"id\": \"rule1\", \n" " \"conditions\": [ \n" " {\"url_id\": 1, \"max\": 3}, \n" " {\"url_id\": 2, \"max\": 5}, \n" " ], \n" " \"actions\": [ \n" " { \n" " \"value\": 2 \n" " } \n" " ], \n" " \"priority\": 200 \n" "}"), json_rule.get())); const char kExtensionId[] = "ext1"; scoped_refptr extension = ExtensionBuilder() .SetManifest(SimpleManifest()) .SetID(kExtensionId) .Build(); base::Time install_time = base::Time::Now(); URLMatcher matcher; std::string error; scoped_ptr rule(Rule::Create(matcher.condition_factory(), NULL, extension.get(), install_time, json_rule, Rule::ConsistencyChecker(), &error)); EXPECT_EQ("", error); ASSERT_TRUE(rule.get()); EXPECT_EQ(kExtensionId, rule->id().first); EXPECT_EQ("rule1", rule->id().second); EXPECT_EQ(200, rule->priority()); const Rule::ConditionSet& condition_set = rule->conditions(); const Rule::ConditionSet::Conditions& conditions = condition_set.conditions(); ASSERT_EQ(2u, conditions.size()); EXPECT_EQ(3, conditions[0]->max_value); EXPECT_EQ(5, conditions[1]->max_value); const Rule::ActionSet& action_set = rule->actions(); const Rule::ActionSet::Actions& actions = action_set.actions(); ASSERT_EQ(1u, actions.size()); EXPECT_EQ(2, actions[0]->increment()); int sum = 0; rule->Apply(&sum); EXPECT_EQ(2, sum); } bool AtLeastOneCondition( const DeclarativeConditionSet* conditions, const DeclarativeActionSet* actions, std::string* error) { if (conditions->conditions().empty()) { *error = "No conditions"; return false; } return true; } TEST(DeclarativeRuleTest, CheckConsistency) { typedef DeclarativeRule Rule; URLMatcher matcher; std::string error; linked_ptr json_rule(new Rule::JsonRule); const char kExtensionId[] = "ext1"; scoped_refptr extension = ExtensionBuilder() .SetManifest(SimpleManifest()) .SetID(kExtensionId) .Build(); ASSERT_TRUE(Rule::JsonRule::Populate( *ParseJson("{ \n" " \"id\": \"rule1\", \n" " \"conditions\": [ \n" " {\"url_id\": 1, \"max\": 3}, \n" " {\"url_id\": 2, \"max\": 5}, \n" " ], \n" " \"actions\": [ \n" " { \n" " \"value\": 2 \n" " } \n" " ], \n" " \"priority\": 200 \n" "}"), json_rule.get())); scoped_ptr rule(Rule::Create(matcher.condition_factory(), NULL, extension.get(), base::Time(), json_rule, base::Bind(AtLeastOneCondition), &error)); EXPECT_TRUE(rule); EXPECT_EQ("", error); ASSERT_TRUE(Rule::JsonRule::Populate( *ParseJson("{ \n" " \"id\": \"rule1\", \n" " \"conditions\": [ \n" " ], \n" " \"actions\": [ \n" " { \n" " \"value\": 2 \n" " } \n" " ], \n" " \"priority\": 200 \n" "}"), json_rule.get())); rule = Rule::Create(matcher.condition_factory(), NULL, extension.get(), base::Time(), json_rule, base::Bind(AtLeastOneCondition), &error); EXPECT_FALSE(rule); EXPECT_EQ("No conditions", error); } } // namespace extensions