blob: 4d0526be0f658296a6a7bdffa1baa049d083aef3 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cstddef>
#include <memory>
#include <string>
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/device_notifications/device_status_icon_renderer.h"
#include "chrome/browser/hid/hid_connection_tracker.h"
#include "chrome/browser/hid/hid_connection_tracker_factory.h"
#include "chrome/browser/hid/hid_system_tray_icon.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/origin.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "base/command_line.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
namespace {
using base::TimeTicks;
using testing::Pair;
using testing::Return;
using testing::UnorderedElementsAre;
using OriginState = HidConnectionTracker::OriginState;
constexpr char kTestProfileName[] = "user@gmail.com";
class MockHidSystemTrayIcon : public HidSystemTrayIcon {
public:
MockHidSystemTrayIcon() : HidSystemTrayIcon(nullptr) {}
MOCK_METHOD(void, StageProfile, (Profile*), (override));
MOCK_METHOD(void, UnstageProfile, (Profile*, bool), (override));
MOCK_METHOD(void, ProfileAdded, (Profile*), (override));
MOCK_METHOD(void, ProfileRemoved, (Profile*), (override));
MOCK_METHOD(void, NotifyConnectionCountUpdated, (Profile*), (override));
};
} // namespace
class HidConnectionTrackerTest : public BrowserWithTestWindowTest {
public:
HidConnectionTrackerTest()
: BrowserWithTestWindowTest(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
HidConnectionTrackerTest(const HidConnectionTrackerTest&) = delete;
HidConnectionTrackerTest& operator=(const HidConnectionTrackerTest&) = delete;
~HidConnectionTrackerTest() override = default;
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
BrowserList::SetLastActive(browser());
auto hid_system_tray_icon = std::make_unique<MockHidSystemTrayIcon>();
hid_system_tray_icon_ = hid_system_tray_icon.get();
TestingBrowserProcess::GetGlobal()->SetHidSystemTrayIcon(
std::move(hid_system_tray_icon));
hid_connection_tracker_ =
HidConnectionTrackerFactory::GetForProfile(profile(), /*create=*/true);
}
void TearDown() override {
// Set the system tray icon to null to avoid uninteresting call to it during
// profile destruction.
TestingBrowserProcess::GetGlobal()->SetHidSystemTrayIcon(nullptr);
BrowserWithTestWindowTest::TearDown();
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
scoped_refptr<const extensions::Extension> 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;
}
void 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_service = extension_system->CreateExtensionService(
base::CommandLine::ForCurrentProcess(), base::FilePath(),
/*autoupdate_enabled=*/false);
}
extension_service->AddExtension(extension);
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
HidConnectionTracker& hid_connection_tracker() {
return *hid_connection_tracker_;
}
MockHidSystemTrayIcon& hid_system_tray_icon() {
return *hid_system_tray_icon_;
}
Profile* CreateTestingProfile(const std::string& profile_name) {
Profile* profile = profile_manager()->CreateTestingProfile(profile_name);
return profile;
}
void 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();
// First connection of the first origin stages the profile.
if (has_system_tray_icon) {
EXPECT_CALL(hid_system_tray_icon(), StageProfile(profile()));
}
hid_connection_tracker().IncrementConnectionCount(
origin_name_pairs[0].first);
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 1);
EXPECT_THAT(hid_connection_tracker().origins(),
UnorderedElementsAre(
Pair(origin_name_pairs[0].first,
OriginState(1, t0, origin_name_pairs[0].second))));
testing::Mock::VerifyAndClearExpectations(&hid_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(hid_system_tray_icon(),
NotifyConnectionCountUpdated(profile()))
.Times(2);
}
hid_connection_tracker().IncrementConnectionCount(
origin_name_pairs[1].first);
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 2);
EXPECT_THAT(hid_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))));
hid_connection_tracker().IncrementConnectionCount(
origin_name_pairs[0].first);
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 3);
EXPECT_THAT(hid_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(&hid_connection_tracker());
// Two origins are removed 5 seconds apart.
if (has_system_tray_icon) {
EXPECT_CALL(hid_system_tray_icon(),
NotifyConnectionCountUpdated(profile()))
.Times(2);
}
hid_connection_tracker().DecrementConnectionCount(
origin_name_pairs[0].first);
hid_connection_tracker().DecrementConnectionCount(
origin_name_pairs[0].first);
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 1);
EXPECT_THAT(hid_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(&hid_connection_tracker());
task_environment()->FastForwardBy(base::Seconds(5));
auto t6 = TimeTicks::Now();
if (has_system_tray_icon) {
EXPECT_CALL(hid_system_tray_icon(),
NotifyConnectionCountUpdated(profile()));
}
hid_connection_tracker().DecrementConnectionCount(
origin_name_pairs[1].first);
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 0);
EXPECT_THAT(hid_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, t6, origin_name_pairs[1].second))));
// The first origin is removed at t11.
if (has_system_tray_icon) {
EXPECT_CALL(hid_system_tray_icon(),
NotifyConnectionCountUpdated(profile()));
}
task_environment()->FastForwardBy(base::Seconds(5));
auto t11 = TimeTicks::Now();
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 0);
EXPECT_THAT(hid_connection_tracker().origins(),
UnorderedElementsAre(
Pair(origin_name_pairs[1].first,
OriginState(0, t6, origin_name_pairs[1].second))));
testing::Mock::VerifyAndClearExpectations(&hid_connection_tracker());
// New connection on the second origin comes in at t11, so it won't be
// removed at t16.
if (has_system_tray_icon) {
EXPECT_CALL(hid_system_tray_icon(),
NotifyConnectionCountUpdated(profile()));
}
hid_connection_tracker().IncrementConnectionCount(
origin_name_pairs[1].first);
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 1);
EXPECT_THAT(hid_connection_tracker().origins(),
UnorderedElementsAre(
Pair(origin_name_pairs[1].first,
OriginState(1, t11, origin_name_pairs[1].second))));
testing::Mock::VerifyAndClearExpectations(&hid_connection_tracker());
task_environment()->FastForwardBy(base::Seconds(5));
auto t16 = TimeTicks::Now();
// Scheduled CleanUpOrigin is no-op at t16.
if (has_system_tray_icon) {
EXPECT_CALL(hid_system_tray_icon(),
NotifyConnectionCountUpdated(profile()))
.Times(0);
EXPECT_CALL(hid_system_tray_icon(),
UnstageProfile(profile(), /*immediate=*/true))
.Times(0);
}
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 1);
EXPECT_THAT(hid_connection_tracker().origins(),
UnorderedElementsAre(
Pair(origin_name_pairs[1].first,
OriginState(1, t11, origin_name_pairs[1].second))));
testing::Mock::VerifyAndClearExpectations(&hid_connection_tracker());
// The last connection of the second origin is gone at t16.
if (has_system_tray_icon) {
EXPECT_CALL(hid_system_tray_icon(),
NotifyConnectionCountUpdated(profile()));
}
hid_connection_tracker().DecrementConnectionCount(
origin_name_pairs[1].first);
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 0);
EXPECT_THAT(hid_connection_tracker().origins(),
UnorderedElementsAre(
Pair(origin_name_pairs[1].first,
OriginState(0, t16, origin_name_pairs[1].second))));
testing::Mock::VerifyAndClearExpectations(&hid_connection_tracker());
// The second origin is removed at time t26, 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(hid_system_tray_icon(),
UnstageProfile(profile(), /*immediate=*/true));
}
task_environment()->FastForwardBy(base::Seconds(10));
EXPECT_EQ(hid_connection_tracker().total_connection_count(), 0);
EXPECT_TRUE(hid_connection_tracker().origins().empty());
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
void 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()}});
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
private:
raw_ptr<HidConnectionTracker, DanglingUntriaged> hid_connection_tracker_;
raw_ptr<MockHidSystemTrayIcon, DanglingUntriaged> hid_system_tray_icon_;
};
#if BUILDFLAG(ENABLE_EXTENSIONS)
TEST_F(HidConnectionTrackerTest, DeviceConnectionExtensionOrigins) {
TestDeviceConnectionExtensionOrigins(/*has_system_tray_icon=*/true);
}
// Test the scenario with null HID system tray icon and it doesn't cause crash.
TEST_F(HidConnectionTrackerTest,
DeviceConnectionExtensionOriginsWithNullSystemTrayIcon) {
TestingBrowserProcess::GetGlobal()->SetHidSystemTrayIcon(nullptr);
TestDeviceConnectionExtensionOrigins(/*has_system_tray_icon=*/false);
}
TEST_F(HidConnectionTrackerTest, ProfileDestroyedExtensionOrigin) {
auto t0 = TimeTicks::Now();
auto* profile_to_be_destroyed = CreateTestingProfile(kTestProfileName);
auto extension = CreateExtensionWithName("Test Extension");
auto origin = extension->origin();
auto* connection_tracker =
HidConnectionTrackerFactory::GetForProfile(profile_to_be_destroyed,
/*create=*/true);
AddExtensionToProfile(profile_to_be_destroyed, extension.get());
EXPECT_CALL(hid_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(&hid_connection_tracker());
EXPECT_CALL(hid_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(&hid_connection_tracker());
// The profile is destroyed at t5.
task_environment()->FastForwardBy(base::Seconds(5));
EXPECT_CALL(hid_system_tray_icon(),
UnstageProfile(profile_to_be_destroyed, /*immediate=*/true));
profile_manager()->DeleteTestingProfile(kTestProfileName);
testing::Mock::VerifyAndClearExpectations(&hid_connection_tracker());
// The connection tracker is destroyed when the profile is destroyed. No
// UnstageProfile is sent to the system tray icon at time t10.
EXPECT_CALL(hid_system_tray_icon(),
UnstageProfile(profile_to_be_destroyed, /*immediate=*/true))
.Times(0);
task_environment()->FastForwardBy(base::Seconds(10));
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)