blob: ce691e0437d087fa0e3205479cbf5dd55660ea6f [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 <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/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/notifications/notification_display_service_tester.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 "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"
#include "extensions/common/value_builder.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
namespace {
constexpr char kExtensionName[] = "Fake extension";
constexpr char kTestProfileName[] = "user@gmail.com";
class MockHidConnectionTracker : public HidConnectionTracker {
public:
explicit MockHidConnectionTracker(Profile* profile)
: HidConnectionTracker(profile) {}
~MockHidConnectionTracker() override = default;
MOCK_METHOD(void, ShowSiteSettings, (const url::Origin& origin), (override));
};
BrowserContextKeyedServiceFactory::TestingFactory
GetHidConnectionTrackerTestingFactory() {
return base::BindRepeating([](content::BrowserContext* browser_context) {
return static_cast<std::unique_ptr<KeyedService>>(
std::make_unique<MockHidConnectionTracker>(
Profile::FromBrowserContext(browser_context)));
});
}
class MockHidSystemTrayIcon : public HidSystemTrayIcon {
public:
MOCK_METHOD(void, AddProfile, (Profile*), (override));
MOCK_METHOD(void, RemoveProfile, (Profile*), (override));
MOCK_METHOD(void, NotifyConnectionCountUpdated, (Profile*), (override));
};
class HidConnectionTrackerTest : public BrowserWithTestWindowTest {
public:
HidConnectionTrackerTest() = default;
HidConnectionTrackerTest(const HidConnectionTrackerTest&) = delete;
HidConnectionTrackerTest& operator=(const HidConnectionTrackerTest&) = delete;
~HidConnectionTrackerTest() override = default;
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
BrowserList::SetLastActive(browser());
// TODO(crbug.com/1399310): Pass testing factory when creating profile.
// Ideally, we should inject MockHidConnectionTracker by overriding
// BrowserWithTestWindowTest::GetTestingFactories(). However, due to the
// fact that:
// 1) TestingProfile::TestingProfile(...) will call BrowserContextShutdown
// as part of setting testing factory.
// 2) HidConnectionTrackerFactory::BrowserContextShutdown() at some point
// need valid profile_metrics::GetBrowserProfileType() as part of
// HidConnectionTrackerFactory::GetForProfile().
// It will hit failure in profile_metrics::GetBrowserProfileType() because
// the profile is not initialized properly before setting testing factory.
// As a result, here set the testing factory for MockHidConnectionTracker
// after profile() is properly initialized.
HidConnectionTrackerFactory::GetInstance()->SetTestingFactory(
profile(), GetHidConnectionTrackerTestingFactory());
display_service_ =
std::make_unique<NotificationDisplayServiceTester>(profile());
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_ = static_cast<MockHidConnectionTracker*>(
HidConnectionTrackerFactory::GetForProfile(profile(), /*create=*/true));
}
std::u16string GetExpectedDeviceConnectedByExtensionNotificationTitle() {
return u"An extension is using a HID device";
}
std::string GetExpectedNotificationId(const url::Origin& origin) {
return base::StringPrintf("webhid.opened.%s.%s",
profile()->UniqueId().c_str(),
origin.host().c_str());
}
void CheckDeviceConnectedNotification(
const url::Origin& origin,
const std::string& name_in_notification_title) {
auto expected_notification_id = GetExpectedNotificationId(origin);
EXPECT_EQ(display_service_
->GetDisplayedNotificationsForType(
NotificationHandler::Type::TRANSIENT)
.size(),
1u);
auto maybe_notification =
display_service_->GetNotification(expected_notification_id);
ASSERT_TRUE(maybe_notification);
#if BUILDFLAG(ENABLE_EXTENSIONS)
EXPECT_EQ(maybe_notification->title(),
GetExpectedDeviceConnectedByExtensionNotificationTitle());
EXPECT_EQ(maybe_notification->message(),
GetExpectedDeviceConnectedByExtensionNotificationMessage(
name_in_notification_title));
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
EXPECT_TRUE(maybe_notification->delegate());
EXPECT_CALL(*hid_connection_tracker_, ShowSiteSettings(origin));
display_service_->SimulateClick(NotificationHandler::Type::TRANSIENT,
expected_notification_id,
/*action_index=*/absl::nullopt,
/*reply=*/absl::nullopt);
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
std::u16string GetExpectedDeviceConnectedByExtensionNotificationMessage(
const std::string& name) {
return base::UTF8ToUTF16(base::StringPrintf(
"Click to manage permissions for \"%s\"", name.c_str()));
}
scoped_refptr<const extensions::Extension> CreateExtensionWithName(
const std::string& extension_name) {
extensions::DictionaryBuilder manifest;
manifest.Set("name", extension_name)
.Set("description", "For testing.")
.Set("version", "0.1")
.Set("manifest_version", 2)
.Set("web_accessible_resources",
extensions::ListBuilder().Append("index.html").Build());
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder().SetManifest(manifest.Build()).Build();
if (!extension) {
return nullptr;
}
extensions::TestExtensionSystem* extension_system =
static_cast<extensions::TestExtensionSystem*>(
extensions::ExtensionSystem::Get(profile()));
extensions::ExtensionService* extension_service =
extension_system->CreateExtensionService(
base::CommandLine::ForCurrentProcess(), base::FilePath(),
/*autoupdate_enabled=*/false);
extension_service->AddExtension(extension.get());
return 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) {
// Ideally, we should be able to pass testing factory when calling profile
// manager's CreateTestingProfile. However, due to the fact that:
// 1) TestingProfile::TestingProfile(...) will call BrowserContextShutdown
// as part of setting testing factory.
// 2) HidConnectionTrackerFactory::BrowserContextShutdown() at some point
// need valid profile_metrics::GetBrowserProfileType() as part of
// HidConnectionTrackerFactory::GetForProfile().
// It will hit failure in profile_metrics::GetBrowserProfileType() because
// the profile is not initialized properly before setting testing factory.
// As a result, here create a profile then call SetTestingFactory to inject
// MockHidConnectionTracker.
Profile* profile = profile_manager()->CreateTestingProfile(profile_name);
HidConnectionTrackerFactory::GetInstance()->SetTestingFactory(
profile, GetHidConnectionTrackerTestingFactory());
return profile;
}
private:
std::unique_ptr<NotificationDisplayServiceTester> display_service_;
raw_ptr<MockHidConnectionTracker> hid_connection_tracker_;
raw_ptr<MockHidSystemTrayIcon> hid_system_tray_icon_;
};
} // namespace
TEST_F(HidConnectionTrackerTest, DeviceConnection) {
EXPECT_CALL(hid_system_tray_icon(), AddProfile(profile()));
hid_connection_tracker().IncrementConnectionCount();
EXPECT_CALL(hid_system_tray_icon(), NotifyConnectionCountUpdated(profile()));
hid_connection_tracker().IncrementConnectionCount();
EXPECT_CALL(hid_system_tray_icon(), NotifyConnectionCountUpdated(profile()));
hid_connection_tracker().DecrementConnectionCount();
EXPECT_CALL(hid_system_tray_icon(), RemoveProfile(profile()));
hid_connection_tracker().DecrementConnectionCount();
}
TEST_F(HidConnectionTrackerTest, DeviceConnectionWithNullSystemTrayIcon) {
// Test the scenario with null HID system tray icon and it doesn't cause
// crash.
TestingBrowserProcess::GetGlobal()->SetHidSystemTrayIcon(nullptr);
hid_connection_tracker().IncrementConnectionCount();
hid_connection_tracker().IncrementConnectionCount();
hid_connection_tracker().DecrementConnectionCount();
hid_connection_tracker().DecrementConnectionCount();
}
TEST_F(HidConnectionTrackerTest, ProfileDestroyed) {
CreateTestingProfile(kTestProfileName);
EXPECT_CALL(hid_system_tray_icon(), AddProfile(profile()));
hid_connection_tracker().IncrementConnectionCount();
EXPECT_CALL(hid_system_tray_icon(), NotifyConnectionCountUpdated(profile()));
hid_connection_tracker().IncrementConnectionCount();
EXPECT_CALL(hid_system_tray_icon(), RemoveProfile(profile()));
profile_manager()->DeleteTestingProfile(kTestProfileName);
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
TEST_F(HidConnectionTrackerTest, DeviceConnectedNotificationByExtension) {
std::string extension_name(kExtensionName);
auto extension = CreateExtensionWithName(extension_name);
hid_connection_tracker().NotifyDeviceConnected(extension->origin());
CheckDeviceConnectedNotification(extension->origin(), extension_name);
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)