| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/device_notifications/device_connection_tracker_unittest.h" |
| |
| #include <memory> |
| |
| #include "chrome/browser/device_notifications/device_system_tray_icon.h" |
| #include "chrome/browser/device_notifications/device_system_tray_icon_renderer.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/test_extension_system.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "extensions/browser/extension_registrar.h" |
| |
| namespace { |
| |
| using base::TimeTicks; |
| using testing::Pair; |
| using testing::Return; |
| using testing::UnorderedElementsAre; |
| |
| using OriginState = DeviceConnectionTracker::OriginState; |
| |
| constexpr char kTestProfileName[] = "user@gmail.com"; |
| |
| } // namespace |
| |
| DeviceConnectionTrackerTestBase::DeviceConnectionTrackerTestBase() |
| : BrowserWithTestWindowTest( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) {} |
| DeviceConnectionTrackerTestBase::~DeviceConnectionTrackerTestBase() = default; |
| |
| void DeviceConnectionTrackerTestBase::SetUp() { |
| BrowserWithTestWindowTest::SetUp(); |
| BrowserList::SetLastActive(browser()); |
| } |
| |
| Profile* DeviceConnectionTrackerTestBase::CreateTestingProfile( |
| const std::string& profile_name) { |
| Profile* profile = profile_manager()->CreateTestingProfile(profile_name); |
| return profile; |
| } |
| |
| void DeviceConnectionTrackerTestBase::TestDeviceConnection( |
| bool has_system_tray_icon, |
| const std::vector<std::pair<url::Origin, std::string>>& origin_name_pairs) { |
| ASSERT_EQ(origin_name_pairs.size(), 2u); |
| auto t0 = TimeTicks::Now(); |
| MockDeviceSystemTrayIcon* mock_device_system_tray_icon = |
| GetMockDeviceSystemTrayIcon(); |
| auto* connection_tracker = GetDeviceConnectionTracker(profile(), true); |
| |
| // First connection of the first origin stages the profile. |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, StageProfile(profile())); |
| } |
| connection_tracker->IncrementConnectionCount(origin_name_pairs[0].first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 1); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[0].first, |
| OriginState(1, t0, origin_name_pairs[0].second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| // The second origin comes in at t1. |
| task_environment()->FastForwardBy(base::Seconds(1)); |
| auto t1 = TimeTicks::Now(); |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())) |
| .Times(2); |
| } |
| connection_tracker->IncrementConnectionCount(origin_name_pairs[1].first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 2); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[0].first, |
| OriginState(1, t0, origin_name_pairs[0].second)), |
| Pair(origin_name_pairs[1].first, |
| OriginState(1, t1, origin_name_pairs[1].second)))); |
| connection_tracker->IncrementConnectionCount(origin_name_pairs[0].first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 3); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[0].first, |
| OriginState(2, t1, origin_name_pairs[0].second)), |
| Pair(origin_name_pairs[1].first, |
| OriginState(1, t1, origin_name_pairs[1].second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| // Two origins are removed 1 seconds apart. |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())) |
| .Times(2); |
| } |
| connection_tracker->DecrementConnectionCount(origin_name_pairs[0].first); |
| connection_tracker->DecrementConnectionCount(origin_name_pairs[0].first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 1); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[0].first, |
| OriginState(0, t1, origin_name_pairs[0].second)), |
| Pair(origin_name_pairs[1].first, |
| OriginState(1, t1, origin_name_pairs[1].second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| task_environment()->FastForwardBy(base::Seconds(1)); |
| auto t2 = TimeTicks::Now(); |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())); |
| } |
| connection_tracker->DecrementConnectionCount(origin_name_pairs[1].first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 0); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[0].first, |
| OriginState(0, t1, origin_name_pairs[0].second)), |
| Pair(origin_name_pairs[1].first, |
| OriginState(0, t2, origin_name_pairs[1].second)))); |
| |
| // The first origin is removed at t4. |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())); |
| } |
| task_environment()->FastForwardBy(base::Seconds(2)); |
| auto t4 = TimeTicks::Now(); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 0); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[1].first, |
| OriginState(0, t2, origin_name_pairs[1].second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| // New connection on the second origin comes in at t4, so it won't be |
| // removed at t5. |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())); |
| } |
| connection_tracker->IncrementConnectionCount(origin_name_pairs[1].first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 1); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[1].first, |
| OriginState(1, t4, origin_name_pairs[1].second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| task_environment()->FastForwardBy(base::Seconds(1)); |
| auto t5 = TimeTicks::Now(); |
| // Scheduled CleanUpOrigin is no-op at t5. |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())) |
| .Times(0); |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| UnstageProfile(profile(), /*immediate=*/true)) |
| .Times(0); |
| } |
| EXPECT_EQ(connection_tracker->total_connection_count(), 1); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[1].first, |
| OriginState(1, t4, origin_name_pairs[1].second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| // The last connection of the second origin is gone at t5. |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())); |
| } |
| connection_tracker->DecrementConnectionCount(origin_name_pairs[1].first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 0); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(origin_name_pairs[1].first, |
| OriginState(0, t5, origin_name_pairs[1].second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| // The second origin is removed at time t8, and the profile is removed from |
| // the system tray icon because there are no active origins on this profile. |
| if (has_system_tray_icon) { |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| UnstageProfile(profile(), /*immediate=*/true)) |
| .Times(1); |
| } |
| task_environment()->FastForwardBy(base::Seconds(3)); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 0); |
| EXPECT_TRUE(connection_tracker->origins().empty()); |
| } |
| |
| void DeviceConnectionTrackerTestBase::TestWhitelistedOrigin( |
| const std::pair<url::Origin, std::string> whitelisted_origin, |
| const std::pair<url::Origin, std::string> non_whitelisted_origin) { |
| auto t0 = TimeTicks::Now(); |
| MockDeviceSystemTrayIcon* mock_device_system_tray_icon = |
| GetMockDeviceSystemTrayIcon(); |
| auto* connection_tracker = GetDeviceConnectionTracker(profile(), true); |
| |
| EXPECT_CALL(*mock_device_system_tray_icon, StageProfile(profile())).Times(0); |
| connection_tracker->IncrementConnectionCount(whitelisted_origin.first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 0); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| EXPECT_CALL(*mock_device_system_tray_icon, StageProfile(profile())); |
| connection_tracker->IncrementConnectionCount(non_whitelisted_origin.first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 1); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(non_whitelisted_origin.first, |
| OriginState(1, t0, non_whitelisted_origin.second)))); |
| testing::Mock::VerifyAndClearExpectations(&connection_tracker); |
| |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| UnstageProfile(profile(), /*immediate=*/true)) |
| .Times(0); |
| connection_tracker->DecrementConnectionCount(whitelisted_origin.first); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 1); |
| EXPECT_THAT(connection_tracker->origins(), |
| UnorderedElementsAre( |
| Pair(non_whitelisted_origin.first, |
| OriginState(1, t0, non_whitelisted_origin.second)))); |
| |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile())); |
| |
| connection_tracker->DecrementConnectionCount(non_whitelisted_origin.first); |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| UnstageProfile(profile(), /*immediate=*/true)); |
| |
| task_environment()->FastForwardBy(base::Seconds(3)); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 0); |
| EXPECT_TRUE(connection_tracker->origins().empty()); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| scoped_refptr<const extensions::Extension> |
| DeviceConnectionTrackerTestBase::CreateExtensionWithName( |
| const std::string& extension_name) { |
| auto manifest = base::Value::Dict() |
| .Set("name", extension_name) |
| .Set("description", "For testing.") |
| .Set("version", "0.1") |
| .Set("manifest_version", 2) |
| .Set("web_accessible_resources", |
| base::Value::List().Append("index.html")); |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder(/*name=*/extension_name) |
| .MergeManifest(std::move(manifest)) |
| .Build(); |
| CHECK(extension); |
| return extension; |
| } |
| |
| scoped_refptr<const extensions::Extension> |
| DeviceConnectionTrackerTestBase::CreateExtensionWithNameAndId( |
| const std::string& extension_name, |
| const std::string& extension_id) { |
| auto manifest = base::Value::Dict() |
| .Set("name", extension_name) |
| .Set("description", "For testing.") |
| .Set("version", "0.1") |
| .Set("manifest_version", 2) |
| .Set("web_accessible_resources", |
| base::Value::List().Append("index.html")); |
| scoped_refptr<const extensions::Extension> extension = |
| extensions::ExtensionBuilder(/*name=*/extension_name) |
| .SetID(extension_id) |
| .MergeManifest(std::move(manifest)) |
| .Build(); |
| DCHECK(extension); |
| return extension; |
| } |
| |
| void DeviceConnectionTrackerTestBase::AddExtensionToProfile( |
| Profile* profile, |
| const extensions::Extension* extension) { |
| extensions::TestExtensionSystem* extension_system = |
| static_cast<extensions::TestExtensionSystem*>( |
| extensions::ExtensionSystem::Get(profile)); |
| extensions::ExtensionService* extension_service = |
| extension_system->extension_service(); |
| if (!extension_service) { |
| extension_system->CreateExtensionService( |
| base::CommandLine::ForCurrentProcess(), base::FilePath(), |
| /*autoupdate_enabled=*/false); |
| } |
| extensions::ExtensionRegistrar::Get(profile)->AddExtension(extension); |
| } |
| |
| void DeviceConnectionTrackerTestBase::TestDeviceConnectionExtensionOrigins( |
| bool has_system_tray_icon) { |
| auto extension1 = CreateExtensionWithName("Test Extension 1"); |
| auto extension2 = CreateExtensionWithName("Test Extension 2"); |
| AddExtensionToProfile(profile(), extension1.get()); |
| AddExtensionToProfile(profile(), extension2.get()); |
| TestDeviceConnection(has_system_tray_icon, |
| {{extension1->origin(), extension1->name()}, |
| {extension2->origin(), extension2->name()}}); |
| } |
| |
| void DeviceConnectionTrackerTestBase::TestSingleProfileWhitelistedExtension( |
| std::string whitelisted_extension_name, |
| std::string whitelisted_extension_id) { |
| auto whitelisted_extension = CreateExtensionWithNameAndId( |
| whitelisted_extension_name, whitelisted_extension_id); |
| auto extension2 = CreateExtensionWithName("Test Extension 2"); |
| AddExtensionToProfile(profile(), whitelisted_extension.get()); |
| AddExtensionToProfile(profile(), extension2.get()); |
| TestWhitelistedOrigin( |
| {whitelisted_extension->origin(), whitelisted_extension->name()}, |
| {extension2->origin(), extension2->name()}); |
| } |
| |
| void DeviceConnectionTrackerTestBase::TestProfileDestroyedExtensionOrigin() { |
| auto t0 = TimeTicks::Now(); |
| auto* profile_to_be_destroyed = CreateTestingProfile(kTestProfileName); |
| auto extension = CreateExtensionWithName("Test Extension"); |
| auto origin = extension->origin(); |
| MockDeviceSystemTrayIcon* mock_device_system_tray_icon = |
| GetMockDeviceSystemTrayIcon(); |
| auto* connection_tracker = |
| GetDeviceConnectionTracker(profile_to_be_destroyed, true); |
| auto* device_connection_tracker = GetDeviceConnectionTracker(profile(), true); |
| AddExtensionToProfile(profile_to_be_destroyed, extension.get()); |
| |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| StageProfile(profile_to_be_destroyed)); |
| connection_tracker->IncrementConnectionCount(origin); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 1); |
| EXPECT_THAT( |
| connection_tracker->origins(), |
| UnorderedElementsAre(Pair(origin, OriginState(1, t0, "Test Extension")))); |
| testing::Mock::VerifyAndClearExpectations(&device_connection_tracker); |
| |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| NotifyConnectionCountUpdated(profile_to_be_destroyed)); |
| connection_tracker->DecrementConnectionCount(origin); |
| EXPECT_EQ(connection_tracker->total_connection_count(), 0); |
| EXPECT_THAT( |
| connection_tracker->origins(), |
| UnorderedElementsAre(Pair(origin, OriginState(0, t0, "Test Extension")))); |
| testing::Mock::VerifyAndClearExpectations(&device_connection_tracker); |
| |
| // The profile is destroyed at t2. |
| task_environment()->FastForwardBy(base::Seconds(2)); |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| UnstageProfile(profile_to_be_destroyed, /*immediate=*/true)) |
| .Times(1); |
| profile_manager()->DeleteTestingProfile(kTestProfileName); |
| testing::Mock::VerifyAndClearExpectations(&device_connection_tracker); |
| |
| // The connection tracker is destroyed when the profile is destroyed. No |
| // UnstageProfile is sent to the system tray icon at time t3. |
| EXPECT_CALL(*mock_device_system_tray_icon, |
| UnstageProfile(profile_to_be_destroyed, /*immediate=*/true)) |
| .Times(0); |
| task_environment()->FastForwardBy(base::Seconds(1)); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |