// Copyright 2015 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 "chrome/browser/android/data_usage/data_use_matcher.h" #include #include #include #include #include "base/macros.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "base/time/tick_clock.h" #include "base/time/time.h" #include "chrome/browser/android/data_usage/external_data_use_observer.h" #include "content/public/browser/browser_thread.h" #include "content/public/test/test_browser_thread_bundle.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" namespace { const char kRegexFoo[] = "http://foo.com/"; const char kLabelFoo[] = "label_foo"; const char kAppFoo[] = "com.example.foo"; const uint32_t kDefaultMatchingRuleExpirationDurationSeconds = 60 * 60 * 24; // 24 hours. class NowTestTickClock : public base::TickClock { public: NowTestTickClock() {} ~NowTestTickClock() override {} base::TimeTicks NowTicks() override { return now_ticks_; } void set_now_ticks(const base::TimeTicks& now_ticks) { now_ticks_ = now_ticks; } private: base::TimeTicks now_ticks_; DISALLOW_COPY_AND_ASSIGN(NowTestTickClock); }; } // namespace namespace chrome { namespace android { class ExternalDataUseObserver; class DataUseMatcherTest : public testing::Test { public: DataUseMatcherTest() : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP), data_use_matcher_(base::WeakPtr(), content::BrowserThread::GetMessageLoopProxyForThread( content::BrowserThread::IO), base::WeakPtr(), base::TimeDelta::FromSeconds( kDefaultMatchingRuleExpirationDurationSeconds)) {} DataUseMatcher* data_use_matcher() { return &data_use_matcher_; } void RegisterURLRegexes(const std::vector& app_package_name, const std::vector& domain_path_regex, const std::vector& label) { data_use_matcher_.RegisterURLRegexes(app_package_name, domain_path_regex, label); } // Returns true if the matching rule at |index| is expired. bool IsExpired(size_t index) { DCHECK_LT(index, data_use_matcher_.matching_rules_.size()); return data_use_matcher_.matching_rules_[index]->expiration() < data_use_matcher_.tick_clock_->NowTicks(); } private: content::TestBrowserThreadBundle thread_bundle_; DataUseMatcher data_use_matcher_; DISALLOW_COPY_AND_ASSIGN(DataUseMatcherTest); }; TEST_F(DataUseMatcherTest, SingleRegex) { const struct { std::string url; std::string regex; bool expect_match; } tests[] = { {"http://www.google.com", "http://www.google.com/", true}, {"http://www.Google.com", "http://www.google.com/", true}, {"http://www.googleacom", "http://www.google.com/", true}, {"http://www.googleaacom", "http://www.google.com/", false}, {"http://www.google.com", "https://www.google.com/", false}, {"http://www.google.com", "{http|https}://www[.]google[.]com/search.*", false}, {"https://www.google.com/search=test", "https://www[.]google[.]com/search.*", true}, {"https://www.googleacom/search=test", "https://www[.]google[.]com/search.*", false}, {"https://www.google.com/Search=test", "https://www[.]google[.]com/search.*", true}, {"www.google.com", "http://www.google.com", false}, {"www.google.com:80", "http://www.google.com", false}, {"http://www.google.com:80", "http://www.google.com", false}, {"http://www.google.com:80/", "http://www.google.com/", true}, {"", "http://www.google.com", false}, {"", "", false}, {"https://www.google.com", "http://www.google.com", false}, }; for (size_t i = 0; i < arraysize(tests); ++i) { std::string label(""); RegisterURLRegexes( // App package name not specified in the matching rule. std::vector(1, std::string()), std::vector(1, tests[i].regex), std::vector(1, "label")); EXPECT_EQ(tests[i].expect_match, data_use_matcher()->MatchesURL(GURL(tests[i].url), &label)) << i; // Verify label matches the expected label. std::string expected_label = ""; if (tests[i].expect_match) expected_label = "label"; EXPECT_EQ(expected_label, label); EXPECT_FALSE(data_use_matcher()->MatchesAppPackageName( "com.example.helloworld", &label)) << i; // Empty package name should not match against empty package name in the // matching rule. EXPECT_FALSE( data_use_matcher()->MatchesAppPackageName(std::string(), &label)) << i; } } TEST_F(DataUseMatcherTest, TwoRegex) { const struct { std::string url; std::string regex1; std::string regex2; bool expect_match; } tests[] = { {"http://www.google.com", "http://www.google.com/", "https://www.google.com/", true}, {"http://www.googleacom", "http://www.google.com/", "http://www.google.com/", true}, {"https://www.google.com", "http://www.google.com/", "https://www.google.com/", true}, {"https://www.googleacom", "http://www.google.com/", "https://www.google.com/", true}, {"http://www.google.com", "{http|https}://www[.]google[.]com/search.*", "", false}, {"http://www.google.com/search=test", "http://www[.]google[.]com/search.*", "https://www[.]google[.]com/search.*", true}, {"https://www.google.com/search=test", "http://www[.]google[.]com/search.*", "https://www[.]google[.]com/search.*", true}, {"http://google.com/search=test", "http://www[.]google[.]com/search.*", "https://www[.]google[.]com/search.*", false}, {"https://www.googleacom/search=test", "", "https://www[.]google[.]com/search.*", false}, {"https://www.google.com/Search=test", "", "https://www[.]google[.]com/search.*", true}, {"www.google.com", "http://www.google.com", "", false}, {"www.google.com:80", "http://www.google.com", "", false}, {"http://www.google.com:80", "http://www.google.com", "", false}, {"", "http://www.google.com", "", false}, {"https://www.google.com", "http://www.google.com", "", false}, }; for (size_t i = 0; i < arraysize(tests); ++i) { std::string got_label(""); std::vector url_regexes; url_regexes.push_back(tests[i].regex1 + "|" + tests[i].regex2); const std::string label("label"); RegisterURLRegexes( std::vector(url_regexes.size(), "com.example.helloworld"), url_regexes, std::vector(url_regexes.size(), label)); EXPECT_EQ(tests[i].expect_match, data_use_matcher()->MatchesURL(GURL(tests[i].url), &got_label)) << i; const std::string expected_label = tests[i].expect_match ? label : std::string(); EXPECT_EQ(expected_label, got_label); EXPECT_TRUE(data_use_matcher()->MatchesAppPackageName( "com.example.helloworld", &got_label)) << i; EXPECT_EQ(label, got_label); } } TEST_F(DataUseMatcherTest, MultipleRegex) { std::vector url_regexes; url_regexes.push_back( "https?://www[.]google[.]com/#q=.*|https?://www[.]google[.]com[.]ph/" "#q=.*|https?://www[.]google[.]com[.]ph/[?]gws_rd=ssl#q=.*"); RegisterURLRegexes( std::vector(url_regexes.size(), std::string()), url_regexes, std::vector(url_regexes.size(), "label")); const struct { std::string url; bool expect_match; } tests[] = { {"", false}, {"http://www.google.com", false}, {"http://www.googleacom", false}, {"https://www.google.com", false}, {"https://www.googleacom", false}, {"https://www.google.com", false}, {"quic://www.google.com/q=test", false}, {"http://www.google.com/q=test", false}, {"http://www.google.com/.q=test", false}, {"http://www.google.com/#q=test", true}, {"https://www.google.com/#q=test", true}, {"https://www.google.com.ph/#q=test+abc", true}, {"https://www.google.com.ph/?gws_rd=ssl#q=test+abc", true}, {"http://www.google.com.ph/#q=test", true}, {"https://www.google.com.ph/#q=test", true}, {"http://www.google.co.in/#q=test", false}, {"http://google.com/#q=test", false}, {"https://www.googleacom/#q=test", false}, {"https://www.google.com/#Q=test", true}, // case in-sensitive {"www.google.com/#q=test", false}, {"www.google.com:80/#q=test", false}, {"http://www.google.com:80/#q=test", true}, {"http://www.google.com:80/search?=test", false}, }; for (size_t i = 0; i < arraysize(tests); ++i) { std::string label(""); EXPECT_EQ(tests[i].expect_match, data_use_matcher()->MatchesURL(GURL(tests[i].url), &label)) << i << " " << tests[i].url; } } TEST_F(DataUseMatcherTest, ChangeRegex) { std::string label; // When no regex is specified, the URL match should fail. EXPECT_FALSE(data_use_matcher()->MatchesURL(GURL(""), &label)); EXPECT_FALSE( data_use_matcher()->MatchesURL(GURL("http://www.google.com"), &label)); std::vector url_regexes; url_regexes.push_back("http://www[.]google[.]com/#q=.*"); url_regexes.push_back("https://www[.]google[.]com/#q=.*"); RegisterURLRegexes( std::vector(url_regexes.size(), std::string()), url_regexes, std::vector(url_regexes.size(), "label")); EXPECT_FALSE(data_use_matcher()->MatchesURL(GURL(""), &label)); EXPECT_TRUE(data_use_matcher()->MatchesURL( GURL("http://www.google.com#q=abc"), &label)); EXPECT_FALSE(data_use_matcher()->MatchesURL( GURL("http://www.google.co.in#q=abc"), &label)); // Change the regular expressions to verify that the new regexes replace // the ones specified before. url_regexes.clear(); url_regexes.push_back("http://www[.]google[.]co[.]in/#q=.*"); url_regexes.push_back("https://www[.]google[.]co[.]in/#q=.*"); RegisterURLRegexes( std::vector(url_regexes.size(), std::string()), url_regexes, std::vector(url_regexes.size(), "label")); EXPECT_FALSE(data_use_matcher()->MatchesURL(GURL(""), &label)); EXPECT_FALSE(data_use_matcher()->MatchesURL( GURL("http://www.google.com#q=abc"), &label)); EXPECT_TRUE(data_use_matcher()->MatchesURL( GURL("http://www.google.co.in#q=abc"), &label)); } TEST_F(DataUseMatcherTest, MultipleAppPackageName) { std::vector url_regexes; url_regexes.push_back( "http://www[.]foo[.]com/#q=.*|https://www[.]foo[.]com/#q=.*"); url_regexes.push_back( "http://www[.]bar[.]com/#q=.*|https://www[.]bar[.]com/#q=.*"); url_regexes.push_back(""); std::vector labels; const char kLabelBar[] = "label_bar"; const char kLabelBaz[] = "label_baz"; labels.push_back(kLabelFoo); labels.push_back(kLabelBar); labels.push_back(kLabelBaz); std::vector app_package_names; const char kAppBar[] = "com.example.bar"; const char kAppBaz[] = "com.example.baz"; app_package_names.push_back(kAppFoo); app_package_names.push_back(kAppBar); app_package_names.push_back(kAppBaz); RegisterURLRegexes(app_package_names, url_regexes, labels); // Test if labels are matched properly for app package names. std::string got_label; EXPECT_TRUE(data_use_matcher()->MatchesAppPackageName(kAppFoo, &got_label)); EXPECT_EQ(kLabelFoo, got_label); EXPECT_TRUE(data_use_matcher()->MatchesAppPackageName(kAppBar, &got_label)); EXPECT_EQ(kLabelBar, got_label); EXPECT_TRUE(data_use_matcher()->MatchesAppPackageName(kAppBaz, &got_label)); EXPECT_EQ(kLabelBaz, got_label); EXPECT_FALSE(data_use_matcher()->MatchesAppPackageName( "com.example.unmatched", &got_label)); EXPECT_EQ(std::string(), got_label); EXPECT_FALSE( data_use_matcher()->MatchesAppPackageName(std::string(), &got_label)); EXPECT_EQ(std::string(), got_label); EXPECT_FALSE( data_use_matcher()->MatchesAppPackageName(std::string(), &got_label)); // An empty URL pattern should not match with any URL pattern. EXPECT_FALSE(data_use_matcher()->MatchesURL(GURL(""), &got_label)); EXPECT_FALSE( data_use_matcher()->MatchesURL(GURL("http://www.baz.com"), &got_label)); } TEST_F(DataUseMatcherTest, ParsePackageField) { const struct { std::string app_package_name; std::string expected_app_package_name; base::TimeDelta expected_expiration_duration; } tests[] = { {"", "", base::TimeDelta::FromSeconds( kDefaultMatchingRuleExpirationDurationSeconds)}, {"|", "|", base::TimeDelta::FromSeconds( kDefaultMatchingRuleExpirationDurationSeconds)}, {"|foo", "|foo", base::TimeDelta::FromSeconds( kDefaultMatchingRuleExpirationDurationSeconds)}, {"com.example.foo", "com.example.foo", base::TimeDelta::FromSeconds( kDefaultMatchingRuleExpirationDurationSeconds)}, {"com.example.foo|", "com.example.foo|", base::TimeDelta::FromSeconds( kDefaultMatchingRuleExpirationDurationSeconds)}, {"com.example.foo|foo", "com.example.foo|foo", base::TimeDelta::FromSeconds( kDefaultMatchingRuleExpirationDurationSeconds)}, {"|0", "", base::TimeDelta::FromMilliseconds(0)}, {"|100", "", base::TimeDelta::FromMilliseconds(100)}, {"com.example.foo|0", "com.example.foo", base::TimeDelta::FromMilliseconds(0)}, {"com.example.foo|10000", "com.example.foo", base::TimeDelta::FromMilliseconds(10000)}, }; NowTestTickClock* tick_clock = new NowTestTickClock(); // Set current time to to Epoch. tick_clock->set_now_ticks(base::TimeTicks::UnixEpoch()); // |tick_clock| will be owned by |data_use_matcher_|. data_use_matcher()->tick_clock_.reset(tick_clock); for (const auto& test : tests) { std::string new_app_package_name; base::TimeTicks expiration; data_use_matcher()->ParsePackageField(test.app_package_name, &new_app_package_name, &expiration); DCHECK_EQ(test.expected_app_package_name, new_app_package_name) << test.app_package_name; DCHECK_EQ(base::TimeTicks::UnixEpoch() + test.expected_expiration_duration, expiration) << test.app_package_name; } } // Tests if the expiration time encoded as milliseconds since epoch is parsed // correctly. TEST_F(DataUseMatcherTest, EncodeExpirationTimeInPackageName) { NowTestTickClock* tick_clock = new NowTestTickClock(); // |tick_clock| will be owned by |data_use_matcher_|. data_use_matcher()->tick_clock_.reset(tick_clock); std::vector url_regexes, labels, app_package_names; url_regexes.push_back(kRegexFoo); labels.push_back(kLabelFoo); // Set current time to to Epoch. tick_clock->set_now_ticks(base::TimeTicks::UnixEpoch()); app_package_names.push_back(base::StringPrintf("%s|%d", kAppFoo, 10000)); RegisterURLRegexes(app_package_names, url_regexes, labels); EXPECT_FALSE(IsExpired(0)); // Fast forward 10 seconds, and matching rule expires. tick_clock->set_now_ticks(base::TimeTicks::UnixEpoch() + base::TimeDelta::FromMilliseconds(10000 + 1)); EXPECT_TRUE(IsExpired(0)); // Empty app package name. app_package_names.clear(); app_package_names.push_back(base::StringPrintf("|%d", 20000)); RegisterURLRegexes(app_package_names, url_regexes, labels); EXPECT_FALSE(IsExpired(0)); // Fast forward 20 seconds, and matching rule expires. tick_clock->set_now_ticks(base::TimeTicks::UnixEpoch() + base::TimeDelta::FromMilliseconds(20000 + 1)); EXPECT_TRUE(IsExpired(0)); } // Tests if the expiration time encoded in Java format is parsed correctly. TEST_F(DataUseMatcherTest, EncodeJavaExpirationTimeInPackageName) { std::vector url_regexes, labels, app_package_names; url_regexes.push_back(kRegexFoo); labels.push_back(kLabelFoo); base::TimeTicks start_ticks = base::TimeTicks::Now(); base::Time expiration_time = base::Time::Now() + base::TimeDelta::FromMilliseconds(10000); app_package_names.push_back(base::StringPrintf( "%s|%lld", kAppFoo, static_cast(expiration_time.ToJavaTime()))); RegisterURLRegexes(app_package_names, url_regexes, labels); EXPECT_FALSE(IsExpired(0)); // Check if expiration duration is close to 10 seconds. EXPECT_GE(base::TimeDelta::FromMilliseconds(10001), data_use_matcher()->matching_rules_[0]->expiration() - data_use_matcher()->tick_clock_->NowTicks()); EXPECT_LE(base::TimeDelta::FromMilliseconds(9999), data_use_matcher()->matching_rules_[0]->expiration() - start_ticks); } // Tests that expired matching rules are ignored by MatchesURL and // MatchesAppPackageName. TEST_F(DataUseMatcherTest, MatchesIgnoresExpiredRules) { std::vector url_regexes, labels, app_package_names; std::string got_label; NowTestTickClock* tick_clock = new NowTestTickClock(); // |tick_clock| will be owned by |data_use_matcher_|. data_use_matcher()->tick_clock_.reset(tick_clock); tick_clock->set_now_ticks(base::TimeTicks::UnixEpoch()); url_regexes.push_back(kRegexFoo); labels.push_back(kLabelFoo); app_package_names.push_back(base::StringPrintf("%s|%d", kAppFoo, 10000)); RegisterURLRegexes(app_package_names, url_regexes, labels); tick_clock->set_now_ticks(base::TimeTicks::UnixEpoch() + base::TimeDelta::FromMilliseconds(1)); EXPECT_FALSE(IsExpired(0)); EXPECT_TRUE(data_use_matcher()->MatchesURL(GURL(kRegexFoo), &got_label)); EXPECT_EQ(kLabelFoo, got_label); EXPECT_TRUE(data_use_matcher()->MatchesAppPackageName(kAppFoo, &got_label)); EXPECT_EQ(kLabelFoo, got_label); // Advance time to make it expired. tick_clock->set_now_ticks(base::TimeTicks::UnixEpoch() + base::TimeDelta::FromMilliseconds(10001)); EXPECT_TRUE(IsExpired(0)); EXPECT_FALSE(data_use_matcher()->MatchesURL(GURL(kRegexFoo), &got_label)); EXPECT_FALSE(data_use_matcher()->MatchesAppPackageName(kAppFoo, &got_label)); } } // namespace android } // namespace chrome