// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/apps/app_shim/app_shim_manager_mac.h"
#include <unistd.h>
#include <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "base/apple/scoped_cftyperef.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_shim/app_shim_host_bootstrap_mac.h"
#include "chrome/browser/apps/app_shim/app_shim_host_mac.h"
#include "chrome/browser/apps/app_shim/code_signature_mac.h"
#include "chrome/browser/profiles/avatar_menu.h"
#include "chrome/browser/web_applications/os_integration/mac/app_shim_registry.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/mac/app_shim.mojom.h"
#include "chrome/services/mac_notifications/public/mojom/mac_notifications.mojom.h"
#include "chrome/test/base/test_browser_window.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace apps {
using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
using ::testing::WithArgs;
class MockDelegate : public AppShimManager::Delegate {
public:
~MockDelegate() override {}
MOCK_METHOD2(ShowAppWindows, bool(Profile*, const std::string&));
MOCK_METHOD2(CloseAppWindows, void(Profile*, const std::string&));
MOCK_METHOD2(AppIsInstalled, bool(Profile*, const std::string&));
MOCK_METHOD2(AppUsesRemoteCocoa, bool(Profile*, const std::string&));
MOCK_METHOD2(AppIsMultiProfile, bool(Profile*, const std::string&));
MOCK_METHOD3(EnableExtension,
void(Profile*, const std::string&, base::OnceCallback<void()>));
MOCK_METHOD7(LaunchApp,
void(Profile*,
const std::string& app_id,
const std::vector<base::FilePath>&,
const std::vector<GURL>&,
const GURL&,
chrome::mojom::AppShimLoginItemRestoreState,
base::OnceClosure));
MOCK_METHOD2(GetAppShortcutsMenuItemInfos,
std::vector<chrome::mojom::ApplicationDockMenuItemPtr>(
Profile*,
const std::string&));
// Conditionally mock LaunchShim. Some tests will execute |launch_callback|
// with a particular value.
MOCK_METHOD4(DoLaunchShim,
void(Profile*,
const std::string&,
web_app::LaunchShimUpdateBehavior,
web_app::ShimLaunchMode));
void LaunchShim(Profile* profile,
const std::string& app_id,
web_app::LaunchShimUpdateBehavior update_behavior,
web_app::ShimLaunchMode launch_mode,
ShimLaunchedCallback launched_callback,
ShimTerminatedCallback terminated_callback) override {
if (launch_shim_callback_capture_) {
*launch_shim_callback_capture_ = std::move(launched_callback);
}
if (terminated_shim_callback_capture_) {
*terminated_shim_callback_capture_ = std::move(terminated_callback);
}
DoLaunchShim(profile, app_id, update_behavior, launch_mode);
}
void SetCaptureShimLaunchedCallback(ShimLaunchedCallback* callback) {
launch_shim_callback_capture_ = callback;
}
void SetCaptureShimTerminatedCallback(ShimTerminatedCallback* callback) {
terminated_shim_callback_capture_ = callback;
}
MOCK_METHOD0(HasNonBookmarkAppWindowsOpen, bool());
void SetAppCanCreateHost(bool should_create_host) {
allow_shim_to_connect_ = should_create_host;
}
bool AppCanCreateHost(Profile* profile, const std::string& app_id) override {
return allow_shim_to_connect_;
}
private:
raw_ptr<ShimLaunchedCallback> launch_shim_callback_capture_ = nullptr;
raw_ptr<ShimTerminatedCallback> terminated_shim_callback_capture_ = nullptr;
bool allow_shim_to_connect_ = true;
};
class TestingAppShimManager : public AppShimManager {
public:
explicit TestingAppShimManager(std::unique_ptr<Delegate> delegate)
: AppShimManager(std::move(delegate)) {}
TestingAppShimManager(const TestingAppShimManager&) = delete;
TestingAppShimManager& operator=(const TestingAppShimManager&) = delete;
~TestingAppShimManager() override { DCHECK(load_profile_callbacks_.empty()); }
MOCK_METHOD1(OnShimFocus, void(AppShimHost* host));
void RealOnShimFocus(AppShimHost* host) { AppShimManager::OnShimFocus(host); }
void SetProfileMenuItems(
std::vector<chrome::mojom::ProfileMenuItemPtr> new_profile_menu_items) {
new_profile_menu_items_ = std::move(new_profile_menu_items);
OnAvatarMenuChanged(nullptr);
}
void RebuildProfileMenuItemsFromAvatarMenu() override {
profile_menu_items_.clear();
for (const auto& item : new_profile_menu_items_) {
profile_menu_items_.push_back(item.Clone());
}
}
void SetAcceptablyCodeSigned(bool is_acceptable_code_signed) {
is_acceptably_code_signed_ = is_acceptable_code_signed;
}
bool IsAcceptablyCodeSigned(audit_token_t audit_token) const override {
return is_acceptably_code_signed_;
}
MOCK_METHOD1(ProfileForPath, Profile*(const base::FilePath&));
MOCK_METHOD1(ProfileForBackgroundShimLaunch,
Profile*(const webapps::AppId& app_id));
void LoadProfileAsync(const base::FilePath& path,
base::OnceCallback<void(Profile*)> callback) override {
CaptureLoadProfileCallback(path, std::move(callback));
}
void WaitForAppRegistryReadyAsync(
Profile* profile,
base::OnceCallback<void()> callback) override {
std::move(callback).Run();
}
MOCK_METHOD1(IsProfileLockedForPath, bool(const base::FilePath&));
void SetHostForCreate(std::unique_ptr<AppShimHost> host_for_create) {
host_for_create_ = std::move(host_for_create);
}
std::unique_ptr<AppShimHost> CreateHost(AppShimHost::Client* client,
const base::FilePath& profile_path,
const std::string& app_id,
bool use_remote_cocoa) override {
DCHECK(host_for_create_);
std::unique_ptr<AppShimHost> result = std::move(host_for_create_);
return result;
}
MOCK_METHOD2(OpenAppURLInBrowserWindow,
void(const base::FilePath&, const GURL& url));
MOCK_METHOD0(LaunchProfilePicker, void());
MOCK_METHOD0(MaybeTerminate, void());
void CaptureLoadProfileCallback(const base::FilePath& path,
base::OnceCallback<void(Profile*)> callback) {
load_profile_callbacks_[path] = std::move(callback);
}
bool RunLoadProfileCallback(const base::FilePath& path, Profile* profile) {
std::move(load_profile_callbacks_[path]).Run(profile);
return load_profile_callbacks_.erase(path);
}
private:
std::map<base::FilePath, base::OnceCallback<void(Profile*)>>
load_profile_callbacks_;
std::unique_ptr<AppShimHost> host_for_create_;
std::vector<chrome::mojom::ProfileMenuItemPtr> new_profile_menu_items_;
bool is_acceptably_code_signed_ = true;
};
class TestingAppShimHostBootstrap : public AppShimHostBootstrap {
public:
TestingAppShimHostBootstrap(
const base::FilePath& profile_path,
const std::string& app_id,
bool is_from_bookmark,
std::optional<chrome::mojom::AppShimLaunchResult>* launch_result)
: AppShimHostBootstrap(AuditTokenForCurrentProcess()),
profile_path_(profile_path),
app_id_(app_id),
is_from_bookmark_(is_from_bookmark),
launch_result_(launch_result),
weak_factory_(this) {}
TestingAppShimHostBootstrap(const TestingAppShimHostBootstrap&) = delete;
TestingAppShimHostBootstrap& operator=(const TestingAppShimHostBootstrap&) =
delete;
void DoTestLaunch(
chrome::mojom::AppShimLaunchType launch_type,
const std::vector<base::FilePath>& files,
const std::vector<GURL>& urls,
chrome::mojom::AppShimLoginItemRestoreState login_item_restore_state,
mojo::PendingReceiver<
mac_notifications::mojom::MacNotificationActionHandler>
notification_action_handler) {
mojo::Remote<chrome::mojom::AppShimHost> host;
auto app_shim_info = chrome::mojom::AppShimInfo::New();
app_shim_info->profile_path = profile_path_;
app_shim_info->app_id = app_id_;
if (is_from_bookmark_) {
app_shim_info->app_url = GURL("https://example.com");
}
app_shim_info->launch_type = launch_type;
app_shim_info->files = files;
app_shim_info->urls = urls;
app_shim_info->login_item_restore_state = login_item_restore_state;
app_shim_info->notification_action_handler =
std::move(notification_action_handler);
OnShimConnected(
host.BindNewPipeAndPassReceiver(), std::move(app_shim_info),
base::BindOnce(&TestingAppShimHostBootstrap::DoTestLaunchDone,
launch_result_));
}
static void DoTestLaunchDone(
std::optional<chrome::mojom::AppShimLaunchResult>* launch_result,
chrome::mojom::AppShimLaunchResult result,
variations::VariationsCommandLine feature_state,
mojo::PendingReceiver<chrome::mojom::AppShim> app_shim_receiver) {
if (launch_result) {
launch_result->emplace(result);
}
}
base::WeakPtr<TestingAppShimHostBootstrap> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
private:
const base::FilePath profile_path_;
const std::string app_id_;
const bool is_from_bookmark_;
// Note that |launch_result_| is optional so that we can track whether or not
// the callback to set it has arrived.
raw_ptr<std::optional<chrome::mojom::AppShimLaunchResult>> launch_result_ =
nullptr;
base::WeakPtrFactory<TestingAppShimHostBootstrap> weak_factory_;
static audit_token_t AuditTokenForCurrentProcess() {
audit_token_t token;
mach_msg_type_number_t size = TASK_AUDIT_TOKEN_COUNT;
int kr = task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)&token,
&size);
CHECK(kr == KERN_SUCCESS) << " Error getting audit token.";
return token;
}
};
const char kTestAppIdA[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const char kTestAppIdB[] = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
class TestAppShim : public chrome::mojom::AppShim,
public mac_notifications::mojom::MacNotificationProvider {
public:
// chrome::mojom::AppShim:
void CreateRemoteCocoaApplication(
mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
receiver) override {}
void CreateCommandDispatcherForWidget(uint64_t widget_id) override {}
void SetBadgeLabel(const std::string& badge_label) override {
badge_label_ = badge_label;
}
void SetUserAttention(
chrome::mojom::AppShimAttentionType attention_type) override {}
void UpdateProfileMenu(std::vector<chrome::mojom::ProfileMenuItemPtr>
profile_menu_items) override {
profile_menu_items_ = std::move(profile_menu_items);
}
void UpdateApplicationDockMenu(
std::vector<chrome::mojom::ApplicationDockMenuItemPtr> dock_menu_items)
override {
dock_menu_items_ = std::move(dock_menu_items);
}
void BindNotificationProvider(
mojo::PendingReceiver<mac_notifications::mojom::MacNotificationProvider>
provider) override {
notification_provider_receiver_.Bind(std::move(provider));
}
void RequestNotificationPermission(
RequestNotificationPermissionCallback callback) override {
request_notification_permission_callback_.SetValue(std::move(callback));
}
void BindChildHistogramFetcherFactory(
mojo::PendingReceiver<metrics::mojom::ChildHistogramFetcherFactory>
receiver) override {}
// mac_notifications::mojom::MacNotificationProvider:
void BindNotificationService(
mojo::PendingReceiver<mac_notifications::mojom::MacNotificationService>
service,
mojo::PendingRemote<
mac_notifications::mojom::MacNotificationActionHandler> handler)
override {}
std::vector<chrome::mojom::ProfileMenuItemPtr> profile_menu_items_;
std::vector<chrome::mojom::ApplicationDockMenuItemPtr> dock_menu_items_;
std::string badge_label_;
mojo::Receiver<mac_notifications::mojom::MacNotificationProvider>
notification_provider_receiver_{this};
base::test::TestFuture<RequestNotificationPermissionCallback>
request_notification_permission_callback_;
};
class TestHost : public AppShimHost {
public:
TestHost(const base::FilePath& profile_path,
const std::string& app_id,
TestingAppShimManager* manager)
: AppShimHost(manager,
app_id,
profile_path,
false /* uses_remote_views */),
test_app_shim_(new TestAppShim),
test_weak_factory_(this) {}
TestHost(const TestHost&) = delete;
TestHost& operator=(const TestHost&) = delete;
~TestHost() override {}
chrome::mojom::AppShim* GetAppShim() const override {
return test_app_shim_.get();
}
// Record whether or not OnBootstrapConnected has been called.
void OnBootstrapConnected(
std::unique_ptr<AppShimHostBootstrap> bootstrap) override {
EXPECT_FALSE(did_connect_to_host_);
did_connect_to_host_ = true;
AppShimHost::OnBootstrapConnected(std::move(bootstrap));
}
bool did_connect_to_host() const { return did_connect_to_host_; }
base::WeakPtr<TestHost> GetWeakPtr() {
return test_weak_factory_.GetWeakPtr();
}
using AppShimHost::FilesOpened;
using AppShimHost::NotificationPermissionStatusChanged;
using AppShimHost::OpenAppWithOverrideUrl;
using AppShimHost::ProfileSelectedFromMenu;
using AppShimHost::ReopenApp;
using AppShimHost::UrlsOpened;
std::unique_ptr<TestAppShim> test_app_shim_;
private:
bool did_connect_to_host_ = false;
base::WeakPtrFactory<TestHost> test_weak_factory_;
};
class AppShimManagerTest : public testing::Test {
protected:
AppShimManagerTest() {}
AppShimManagerTest(const AppShimManagerTest&) = delete;
AppShimManagerTest& operator=(const AppShimManagerTest&) = delete;
~AppShimManagerTest() override {}
void SetUp() override {
profile_path_a_ = profile_a_.GetPath();
profile_path_b_ = profile_b_.GetPath();
profile_path_c_ = profile_c_.GetPath();
const base::FilePath user_data_dir = profile_path_a_.DirName();
local_state_ = std::make_unique<TestingPrefServiceSimple>();
AppShimRegistry::Get()->RegisterLocalPrefs(local_state_->registry());
AppShimRegistry::Get()->SetPrefServiceAndUserDataDirForTesting(
local_state_.get(), user_data_dir);
std::unique_ptr<MockDelegate> delegate = std::make_unique<MockDelegate>();
delegate_ = delegate.get();
manager_ = std::make_unique<TestingAppShimManager>(std::move(delegate));
AppShimHostBootstrap::SetClient(manager_.get());
bootstrap_aa_ = (new TestingAppShimHostBootstrap(
profile_path_a_, kTestAppIdA,
true /* is_from_bookmark */, &bootstrap_aa_result_))
->GetWeakPtr();
bootstrap_ba_ = (new TestingAppShimHostBootstrap(
profile_path_b_, kTestAppIdA,
true /* is_from_bookmark */, &bootstrap_ba_result_))
->GetWeakPtr();
bootstrap_ca_ = (new TestingAppShimHostBootstrap(
profile_path_c_, kTestAppIdA,
true /* is_from_bookmark */, &bootstrap_ca_result_))
->GetWeakPtr();
bootstrap_xa_ = (new TestingAppShimHostBootstrap(
base::FilePath(), kTestAppIdA,
true /* is_from_bookmark */, &bootstrap_xa_result_))
->GetWeakPtr();
bootstrap_ab_ = (new TestingAppShimHostBootstrap(
profile_path_a_, kTestAppIdB,
false /* is_from_bookmark */, &bootstrap_ab_result_))
->GetWeakPtr();
bootstrap_bb_ = (new TestingAppShimHostBootstrap(
profile_path_b_, kTestAppIdB,
false /* is_from_bookmark */, &bootstrap_bb_result_))
->GetWeakPtr();
bootstrap_aa_duplicate_ =
(new TestingAppShimHostBootstrap(profile_path_a_, kTestAppIdA,
true /* is_from_bookmark */,
&bootstrap_aa_duplicate_result_))
->GetWeakPtr();
bootstrap_aa_thethird_ =
(new TestingAppShimHostBootstrap(profile_path_a_, kTestAppIdA,
true /* is_from_bookmark */,
&bootstrap_aa_thethird_result_))
->GetWeakPtr();
host_aa_unique_ = std::make_unique<TestHost>(profile_path_a_, kTestAppIdA,
manager_.get());
host_ab_unique_ = std::make_unique<TestHost>(profile_path_a_, kTestAppIdB,
manager_.get());
host_ba_unique_ = std::make_unique<TestHost>(profile_path_b_, kTestAppIdA,
manager_.get());
host_bb_unique_ = std::make_unique<TestHost>(profile_path_b_, kTestAppIdB,
manager_.get());
host_aa_duplicate_unique_ = std::make_unique<TestHost>(
profile_path_a_, kTestAppIdA, manager_.get());
host_aa_ = host_aa_unique_->GetWeakPtr();
host_ab_ = host_ab_unique_->GetWeakPtr();
host_ba_ = host_ba_unique_->GetWeakPtr();
host_bb_ = host_bb_unique_->GetWeakPtr();
base::FilePath extension_path("/fake/path");
EXPECT_CALL(*delegate_, AppIsMultiProfile(_, kTestAppIdA))
.WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_, AppIsMultiProfile(_, kTestAppIdB))
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_, AppUsesRemoteCocoa(_, _))
.WillRepeatedly(Return(true));
{
auto item_a = chrome::mojom::ProfileMenuItem::New();
item_a->profile_path = profile_path_a_;
item_a->menu_index = 0;
auto item_b = chrome::mojom::ProfileMenuItem::New();
item_b->profile_path = profile_path_b_;
item_b->menu_index = 1;
std::vector<chrome::mojom::ProfileMenuItemPtr> items;
items.push_back(std::move(item_a));
items.push_back(std::move(item_b));
manager_->SetProfileMenuItems(std::move(items));
}
// Tests that expect this call will override it.
EXPECT_CALL(*manager_, OpenAppURLInBrowserWindow(_, _)).Times(0);
EXPECT_CALL(*manager_, IsProfileLockedForPath(profile_path_a_))
.WillRepeatedly(Return(false));
EXPECT_CALL(*manager_, ProfileForPath(profile_path_a_))
.WillRepeatedly(Return(&profile_a_));
EXPECT_CALL(*manager_, IsProfileLockedForPath(profile_path_b_))
.WillRepeatedly(Return(false));
EXPECT_CALL(*manager_, ProfileForPath(profile_path_b_))
.WillRepeatedly(Return(&profile_b_));
EXPECT_CALL(*manager_, IsProfileLockedForPath(profile_path_c_))
.WillRepeatedly(Return(false));
EXPECT_CALL(*manager_, ProfileForPath(profile_path_c_))
.WillRepeatedly(Return(&profile_c_));
EXPECT_CALL(*delegate_, AppIsInstalled(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_, AppIsInstalled(&profile_b_, kTestAppIdA))
.WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_, AppIsInstalled(&profile_c_, kTestAppIdA))
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_, AppIsInstalled(_, kTestAppIdB))
.WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_, LaunchApp(_, _, _, _, _, _, _))
.WillRepeatedly(Return());
}
void TearDown() override {
host_aa_unique_.reset();
host_ab_unique_.reset();
host_ba_unique_.reset();
host_bb_unique_.reset();
host_aa_duplicate_unique_.reset();
delegate_ = nullptr;
manager_->SetHostForCreate(nullptr);
manager_.reset();
// Delete the bootstraps via their weak pointers if they haven't been
// deleted yet. Note that this must be done after the profiles and hosts
// have been destroyed (because they may now own the bootstraps).
delete bootstrap_aa_.get();
delete bootstrap_ba_.get();
delete bootstrap_ca_.get();
delete bootstrap_xa_.get();
delete bootstrap_ab_.get();
delete bootstrap_bb_.get();
delete bootstrap_aa_duplicate_.get();
delete bootstrap_aa_thethird_.get();
AppShimHostBootstrap::SetClient(nullptr);
AppShimRegistry::Get()->SetPrefServiceAndUserDataDirForTesting(
nullptr, base::FilePath());
}
void DoShimLaunch(
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap,
std::unique_ptr<TestHost> host,
chrome::mojom::AppShimLaunchType launch_type,
const std::vector<base::FilePath>& files,
const std::vector<GURL>& urls,
chrome::mojom::AppShimLoginItemRestoreState login_item_restore_state,
mojo::PendingReceiver<
mac_notifications::mojom::MacNotificationActionHandler>
notification_action_handler = mojo::NullReceiver()) {
if (host) {
manager_->SetHostForCreate(std::move(host));
}
bootstrap->DoTestLaunch(launch_type, files, urls, login_item_restore_state,
std::move(notification_action_handler));
}
void NormalLaunch(base::WeakPtr<TestingAppShimHostBootstrap> bootstrap,
std::unique_ptr<TestHost> host) {
DoShimLaunch(bootstrap, std::move(host),
chrome::mojom::AppShimLaunchType::kNormal,
std::vector<base::FilePath>(), std::vector<GURL>(),
chrome::mojom::AppShimLoginItemRestoreState::kNone);
}
void RegisterOnlyLaunch(base::WeakPtr<TestingAppShimHostBootstrap> bootstrap,
std::unique_ptr<TestHost> host) {
DoShimLaunch(bootstrap, std::move(host),
chrome::mojom::AppShimLaunchType::kRegisterOnly,
std::vector<base::FilePath>(), std::vector<GURL>(),
chrome::mojom::AppShimLoginItemRestoreState::kNone);
}
void NotificationActionLaunch(
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap,
std::unique_ptr<TestHost> host,
mac_notifications::mojom::NotificationActionInfoPtr notification) {
mojo::Remote<mac_notifications::mojom::MacNotificationActionHandler>
notification_remote;
DoShimLaunch(bootstrap, std::move(host),
chrome::mojom::AppShimLaunchType::kNotificationAction,
std::vector<base::FilePath>(), std::vector<GURL>(),
chrome::mojom::AppShimLoginItemRestoreState::kNone,
notification_remote.BindNewPipeAndPassReceiver());
notification_remote->OnNotificationAction(std::move(notification));
}
// Completely launch a shim host and leave it running.
void LaunchAndActivate(base::WeakPtr<TestingAppShimHostBootstrap> bootstrap,
std::unique_ptr<TestHost> host_unique,
Profile* profile) {
base::WeakPtr<TestHost> host = host_unique->GetWeakPtr();
NormalLaunch(bootstrap, std::move(host_unique));
EXPECT_EQ(host.get(), manager_->FindHost(profile, host->GetAppId()));
EXPECT_CALL(*manager_, OnShimFocus(host.get()));
manager_->OnAppActivated(profile, host->GetAppId());
EXPECT_TRUE(host->did_connect_to_host());
}
// Simulates a focus request coming from a running app shim.
void ShimNormalFocus(TestHost* host) {
EXPECT_CALL(*manager_, OnShimFocus(host))
.WillOnce(
Invoke(manager_.get(), &TestingAppShimManager::RealOnShimFocus));
manager_->OnShimFocus(host);
}
content::BrowserTaskEnvironment task_environment_;
raw_ptr<MockDelegate> delegate_ = nullptr;
std::unique_ptr<TestingAppShimManager> manager_;
base::FilePath profile_path_a_;
base::FilePath profile_path_b_;
base::FilePath profile_path_c_;
TestingProfile profile_a_;
TestingProfile profile_b_;
TestingProfile profile_c_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_aa_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_ba_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_ca_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_xa_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_ab_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_bb_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_aa_duplicate_;
base::WeakPtr<TestingAppShimHostBootstrap> bootstrap_aa_thethird_;
std::optional<chrome::mojom::AppShimLaunchResult> bootstrap_aa_result_;
std::optional<chrome::mojom::AppShimLaunchResult> bootstrap_ba_result_;
std::optional<chrome::mojom::AppShimLaunchResult> bootstrap_ca_result_;
std::optional<chrome::mojom::AppShimLaunchResult> bootstrap_xa_result_;
std::optional<chrome::mojom::AppShimLaunchResult> bootstrap_ab_result_;
std::optional<chrome::mojom::AppShimLaunchResult> bootstrap_bb_result_;
std::optional<chrome::mojom::AppShimLaunchResult>
bootstrap_aa_duplicate_result_;
std::optional<chrome::mojom::AppShimLaunchResult>
bootstrap_aa_thethird_result_;
// Unique ptr to the TestsHosts used by the tests. These are passed by
// std::move during tests. To access them after they have been passed, use
// the WeakPtr versions.
std::unique_ptr<TestHost> host_aa_unique_;
std::unique_ptr<TestHost> host_ab_unique_;
std::unique_ptr<TestHost> host_ba_unique_;
std::unique_ptr<TestHost> host_bb_unique_;
std::unique_ptr<TestHost> host_aa_duplicate_unique_;
base::WeakPtr<TestHost> host_aa_;
base::WeakPtr<TestHost> host_ab_;
base::WeakPtr<TestHost> host_ba_;
base::WeakPtr<TestHost> host_bb_;
base::test::ScopedFeatureList scoped_feature_list_;
private:
std::unique_ptr<TestingPrefServiceSimple> local_state_;
};
TEST_F(AppShimManagerTest, LaunchProfileNotFound) {
// Bad profile path, opens a bookmark app in a new window.
EXPECT_CALL(*manager_, ProfileForPath(profile_path_a_))
.WillRepeatedly(Return(static_cast<Profile*>(nullptr)));
NormalLaunch(bootstrap_aa_, nullptr);
EXPECT_CALL(*manager_, OpenAppURLInBrowserWindow(profile_path_a_, _));
manager_->RunLoadProfileCallback(profile_path_a_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kProfileNotFound,
*bootstrap_aa_result_);
}
TEST_F(AppShimManagerTest, LaunchProfileNotFoundNotBookmark) {
// Bad profile path, not a bookmark app, doesn't open anything.
EXPECT_CALL(*manager_, ProfileForPath(profile_path_a_))
.WillRepeatedly(Return(static_cast<Profile*>(nullptr)));
NormalLaunch(bootstrap_ab_, nullptr);
manager_->RunLoadProfileCallback(profile_path_a_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kProfileNotFound,
*bootstrap_ab_result_);
}
TEST_F(AppShimManagerTest, LaunchProfileIsLocked) {
// Profile is locked.
EXPECT_CALL(*manager_, IsProfileLockedForPath(profile_path_a_))
.WillOnce(Return(true));
EXPECT_CALL(*manager_, LaunchProfilePicker());
NormalLaunch(bootstrap_aa_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kProfileLocked,
*bootstrap_aa_result_);
}
TEST_F(AppShimManagerTest, LaunchAppNotFound) {
// App not found.
EXPECT_CALL(*delegate_, AppIsInstalled(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_, EnableExtension(&profile_a_, kTestAppIdA, _))
.WillOnce(RunOnceCallback<2>());
EXPECT_CALL(*manager_, OpenAppURLInBrowserWindow(profile_path_a_, _));
NormalLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kAppNotFound,
*bootstrap_aa_result_);
}
TEST_F(AppShimManagerTest, LaunchAppNotEnabled) {
// App not found.
EXPECT_CALL(*delegate_, AppIsInstalled(&profile_a_, kTestAppIdA))
.WillOnce(Return(false))
.WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_, EnableExtension(&profile_a_, kTestAppIdA, _))
.WillOnce(RunOnceCallback<2>());
NormalLaunch(bootstrap_aa_, std::move(host_aa_unique_));
}
TEST_F(AppShimManagerTest, LaunchAndCloseShim) {
// Normal startup.
NormalLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
NormalLaunch(bootstrap_ab_, std::move(host_ab_unique_));
EXPECT_EQ(host_ab_.get(), manager_->FindHost(&profile_a_, kTestAppIdB));
std::vector<base::FilePath> some_file(1, base::FilePath("some_file"));
std::vector<GURL> some_url(1, GURL("web+test://foo"));
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdB, some_file, some_url, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _));
DoShimLaunch(bootstrap_bb_, std::move(host_bb_unique_),
chrome::mojom::AppShimLaunchType::kNormal, some_file, some_url,
chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_EQ(host_bb_.get(), manager_->FindHost(&profile_b_, kTestAppIdB));
// Activation when there is a registered shim finishes launch with success and
// focuses the app.
EXPECT_CALL(*manager_, OnShimFocus(host_aa_.get()));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
// Starting and closing a second host does nothing.
DoShimLaunch(bootstrap_aa_duplicate_, std::move(host_aa_duplicate_unique_),
chrome::mojom::AppShimLaunchType::kNormal, some_file, some_url,
chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kDuplicateHost,
*bootstrap_aa_duplicate_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Normal close.
manager_->OnShimProcessDisconnected(host_aa_.get());
EXPECT_FALSE(manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_EQ(host_aa_.get(), nullptr);
}
TEST_F(AppShimManagerTest, RunOnOsLoginLaunchAndCloseShim) {
NormalLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
NormalLaunch(bootstrap_ab_, std::move(host_ab_unique_));
EXPECT_EQ(host_ab_.get(), manager_->FindHost(&profile_a_, kTestAppIdB));
// Run on OS Login Launch
std::vector<base::FilePath> some_file(1, base::FilePath("some_file"));
std::vector<GURL> some_url(1, GURL("web+test://foo"));
EXPECT_CALL(
*delegate_,
LaunchApp(&profile_b_, kTestAppIdB, some_file, some_url, _,
chrome::mojom::AppShimLoginItemRestoreState::kWindowed, _));
DoShimLaunch(bootstrap_bb_, std::move(host_bb_unique_),
chrome::mojom::AppShimLaunchType::kNormal, some_file, some_url,
chrome::mojom::AppShimLoginItemRestoreState::kWindowed);
EXPECT_EQ(host_bb_.get(), manager_->FindHost(&profile_b_, kTestAppIdB));
// Activation when there is a registered shim finishes launch with success and
// focuses the app.
EXPECT_CALL(*manager_, OnShimFocus(host_aa_.get()));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
// Starting and closing a second host does nothing.
DoShimLaunch(bootstrap_aa_duplicate_, std::move(host_aa_duplicate_unique_),
chrome::mojom::AppShimLaunchType::kNormal, some_file, some_url,
chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kDuplicateHost,
*bootstrap_aa_duplicate_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Normal close.
manager_->OnShimProcessDisconnected(host_aa_.get());
EXPECT_FALSE(manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_EQ(host_aa_.get(), nullptr);
}
TEST_F(AppShimManagerTest, AppLaunchCancelled) {
// Validate that if no browser is registered during a launch that
// OnAppLaunchCancelled removes the host and closes the app.
NormalLaunch(bootstrap_bb_, std::move(host_bb_unique_));
EXPECT_EQ(host_bb_.get(), manager_->FindHost(&profile_b_, kTestAppIdB));
EXPECT_CALL(*manager_, MaybeTerminate()).WillOnce(Return());
manager_->OnAppLaunchCancelled(&profile_b_, kTestAppIdB);
EXPECT_FALSE(manager_->FindHost(&profile_b_, kTestAppIdB));
EXPECT_EQ(host_bb_.get(), nullptr);
// Validate that if a browser is registered during a launch
// that OnAppLaunchCancelled is an no-op
NormalLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Notify manager that a new browser has been associated with the app.
auto browser_window = std::make_unique<TestBrowserWindow>();
std::string app_name = web_app::GenerateApplicationNameFromAppId(kTestAppIdA);
Browser::CreateParams params = Browser::CreateParams::CreateForApp(
app_name, true, browser_window->GetBounds(), &profile_a_, true);
params.window = browser_window.get();
auto browser = std::unique_ptr<Browser>(Browser::Create(params));
manager_->OnBrowserAdded(browser.get());
// Validate that OnAppLaunchCancelled does not close the app,
// and that the state is still valid.
EXPECT_CALL(*manager_, MaybeTerminate()).Times(0);
manager_->OnAppLaunchCancelled(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Removing the browser should close the app.
EXPECT_CALL(*manager_, MaybeTerminate()).WillOnce(Return());
manager_->OnBrowserRemoved(browser.get());
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, AppLifetime) {
scoped_feature_list_.InitWithFeatures({features::kAppShimNewCloseBehavior},
{});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
// When the app activates, a host is created. If there is no shim, one is
// launched.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Normal shim launch adds an entry in the map.
// App should not be launched here, but return success to the shim.
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, _, _, _, _, _))
.Times(0);
RegisterOnlyLaunch(bootstrap_aa_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Return no app windows for OnShimFocus. This will do nothing.
EXPECT_CALL(*delegate_, ShowAppWindows(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, _, _, _, _, _))
.Times(0);
ShimNormalFocus(host_aa_.get());
// Return no app windows for OnShimReopen. This will result in a launch call.
EXPECT_CALL(*delegate_, ShowAppWindows(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, _, _, _, _, _))
.Times(1);
host_aa_->ReopenApp();
// Return one window. This should do nothing.
EXPECT_CALL(*delegate_, ShowAppWindows(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
host_aa_->ReopenApp();
// Open files should trigger a launch with those files.
std::vector<base::FilePath> some_file(1, base::FilePath("some_file"));
EXPECT_CALL(
*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, some_file, std::vector<GURL>(),
GURL(), chrome::mojom::AppShimLoginItemRestoreState::kNone, _));
host_aa_->FilesOpened(some_file);
// Open urls should trigger a launch with those urls
std::vector<GURL> some_url(1, GURL("web+test://foo"));
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, std::vector<base::FilePath>(),
some_url, GURL(),
chrome::mojom::AppShimLoginItemRestoreState::kNone, _));
host_aa_->UrlsOpened(some_url);
// Open app with override url should trigger a launch with that url
GURL some_override_url("https://some-override-url.com");
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, std::vector<base::FilePath>(),
std::vector<GURL>(), some_override_url,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _));
host_aa_->OpenAppWithOverrideUrl(some_override_url);
// OnAppDeactivated should not close the shim.
EXPECT_CALL(*manager_, MaybeTerminate()).Times(0);
manager_->OnAppDeactivated(&profile_a_, kTestAppIdA);
EXPECT_NE(nullptr, host_aa_.get());
// Process disconnect will cause the shim to close.
EXPECT_CALL(*manager_, MaybeTerminate()).WillOnce(Return());
manager_->OnShimProcessDisconnected(host_aa_.get());
EXPECT_EQ(nullptr, host_aa_.get());
}
TEST_F(AppShimManagerTest, AppLifetimeOld) {
scoped_feature_list_.InitWithFeatures({},
{features::kAppShimNewCloseBehavior});
// When the app activates, a host is created. If there is no shim, one is
// launched.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Normal shim launch adds an entry in the map.
// App should not be launched here, but return success to the shim.
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, _, _, _, _, _))
.Times(0);
RegisterOnlyLaunch(bootstrap_aa_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Return no app windows for OnShimFocus. This will do nothing.
EXPECT_CALL(*delegate_, ShowAppWindows(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, _, _, _, _, _))
.Times(0);
ShimNormalFocus(host_aa_.get());
// Return no app windows for OnShimReopen. This will result in a launch call.
EXPECT_CALL(*delegate_, ShowAppWindows(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, _, _, _, _, _))
.Times(1);
host_aa_->ReopenApp();
// Return one window. This should do nothing.
EXPECT_CALL(*delegate_, ShowAppWindows(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, _, _, _, _, _))
.Times(0);
host_aa_->ReopenApp();
// Open files should trigger a launch with those files.
std::vector<base::FilePath> some_file(1, base::FilePath("some_file"));
EXPECT_CALL(*delegate_, LaunchApp(&profile_a_, kTestAppIdA, some_file,
std::vector<GURL>(), GURL(), _, _));
host_aa_->FilesOpened(some_file);
// Open urls should trigger a launch with those urls
std::vector<GURL> some_url(1, GURL("web+test://foo"));
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, std::vector<base::FilePath>(),
some_url, GURL(), _, _));
host_aa_->UrlsOpened(some_url);
// Open app with override url should trigger a launch with that url
GURL some_override_url("https://some-override-url.com");
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, std::vector<base::FilePath>(),
std::vector<GURL>(), some_override_url, _, _));
host_aa_->OpenAppWithOverrideUrl(some_override_url);
// Process disconnect will cause the host to be deleted.
manager_->OnShimProcessDisconnected(host_aa_.get());
EXPECT_EQ(nullptr, host_aa_.get());
// OnAppDeactivated should trigger a MaybeTerminate call.
EXPECT_CALL(*manager_, MaybeTerminate()).WillOnce(Return());
manager_->OnAppDeactivated(&profile_a_, kTestAppIdA);
}
TEST_F(AppShimManagerTest, FailToLaunch) {
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
// When the app activates, it requests a launch.
ShimLaunchedCallback launch_callback;
delegate_->SetCaptureShimLaunchedCallback(&launch_callback);
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_TRUE(launch_callback);
// Run the callback claiming that the launch failed. This should trigger
// another launch, this time forcing shim recreation.
EXPECT_CALL(
*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kRecreateUnconditionally,
web_app::ShimLaunchMode::kNormal));
std::move(launch_callback).Run(base::Process());
EXPECT_TRUE(launch_callback);
// Report that the launch failed. This should trigger deletion of the host.
EXPECT_NE(nullptr, host_aa_.get());
std::move(launch_callback).Run(base::Process());
EXPECT_EQ(nullptr, host_aa_.get());
}
TEST_F(AppShimManagerTest, FailToConnect) {
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
// When the app activates, it requests a launch.
ShimLaunchedCallback launched_callback;
delegate_->SetCaptureShimLaunchedCallback(&launched_callback);
ShimTerminatedCallback terminated_callback;
delegate_->SetCaptureShimTerminatedCallback(&terminated_callback);
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_TRUE(launched_callback);
EXPECT_TRUE(terminated_callback);
// Run the launch callback claiming that the launch succeeded.
std::move(launched_callback).Run(base::Process(5));
EXPECT_FALSE(launched_callback);
EXPECT_TRUE(terminated_callback);
// Report that the process terminated. This should trigger a re-create and
// re-launch.
EXPECT_CALL(
*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kRecreateUnconditionally,
web_app::ShimLaunchMode::kNormal));
std::move(terminated_callback).Run();
EXPECT_TRUE(launched_callback);
EXPECT_TRUE(terminated_callback);
// Run the launch callback claiming that the launch succeeded.
std::move(launched_callback).Run(base::Process(7));
EXPECT_FALSE(launched_callback);
EXPECT_TRUE(terminated_callback);
// Report that the process terminated again. This should trigger deletion of
// the host.
EXPECT_NE(nullptr, host_aa_.get());
std::move(terminated_callback).Run();
EXPECT_EQ(nullptr, host_aa_.get());
}
TEST_F(AppShimManagerTest, FailCodeSignature) {
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
manager_->SetAcceptablyCodeSigned(false);
ShimLaunchedCallback launched_callback;
delegate_->SetCaptureShimLaunchedCallback(&launched_callback);
ShimTerminatedCallback terminated_callback;
delegate_->SetCaptureShimTerminatedCallback(&terminated_callback);
// Fail to code-sign. This should result in a host being created, and a launch
// having been requested.
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
NormalLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_TRUE(launched_callback);
EXPECT_TRUE(terminated_callback);
EXPECT_FALSE(host_aa_->HasBootstrapConnected());
// Run the launch callback claiming that the launch succeeded.
std::move(launched_callback).Run(base::Process(5));
EXPECT_FALSE(launched_callback);
EXPECT_TRUE(terminated_callback);
EXPECT_FALSE(host_aa_->HasBootstrapConnected());
// Simulate the register call that then fails due to signature failing.
RegisterOnlyLaunch(bootstrap_aa_duplicate_, std::move(host_aa_unique_));
EXPECT_FALSE(host_aa_->HasBootstrapConnected());
// Simulate the termination after the register failed.
manager_->SetAcceptablyCodeSigned(true);
EXPECT_CALL(
*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kRecreateUnconditionally,
web_app::ShimLaunchMode::kNormal));
std::move(terminated_callback).Run();
EXPECT_TRUE(launched_callback);
EXPECT_TRUE(terminated_callback);
RegisterOnlyLaunch(bootstrap_aa_thethird_, std::move(host_aa_unique_));
EXPECT_TRUE(host_aa_->HasBootstrapConnected());
}
TEST_F(AppShimManagerTest, MaybeTerminate) {
scoped_feature_list_.InitWithFeatures({features::kAppShimNewCloseBehavior},
{});
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdB,
profile_path_a_);
// Launch shims, adding entries in the map.
RegisterOnlyLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
RegisterOnlyLaunch(bootstrap_ab_, std::move(host_ab_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_ab_result_);
EXPECT_EQ(host_ab_.get(), manager_->FindHost(&profile_a_, kTestAppIdB));
// Quitting when there's another shim should not terminate.
EXPECT_CALL(*manager_, MaybeTerminate()).Times(0);
manager_->OnAppDeactivated(&profile_a_, kTestAppIdA);
// Quitting when it's the last shim should not terminate in the new behavior.
EXPECT_CALL(*manager_, MaybeTerminate()).Times(0);
manager_->OnAppDeactivated(&profile_a_, kTestAppIdB);
}
TEST_F(AppShimManagerTest, MaybeTerminateOnUninstall) {
scoped_feature_list_.InitWithFeatures({features::kAppShimNewCloseBehavior},
{});
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdB,
profile_path_a_);
// Launch shims, adding entries in the map.
RegisterOnlyLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
RegisterOnlyLaunch(bootstrap_ab_, std::move(host_ab_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_ab_result_);
EXPECT_EQ(host_ab_.get(), manager_->FindHost(&profile_a_, kTestAppIdB));
// Quitting when there's another shim should not terminate.
AppShimRegistry::Get()->OnAppUninstalledForProfile(kTestAppIdA,
profile_path_a_);
EXPECT_CALL(*manager_, MaybeTerminate()).Times(0);
manager_->OnAppDeactivated(&profile_a_, kTestAppIdA);
// Quitting when it's the last shim and the app is uninstalled should
// terminate.
AppShimRegistry::Get()->OnAppUninstalledForProfile(kTestAppIdB,
profile_path_a_);
EXPECT_CALL(*manager_, MaybeTerminate()).Times(1);
manager_->OnAppDeactivated(&profile_a_, kTestAppIdB);
}
TEST_F(AppShimManagerTest, MaybeTerminateOld) {
scoped_feature_list_.InitWithFeatures({},
{features::kAppShimNewCloseBehavior});
// Launch shims, adding entries in the map.
RegisterOnlyLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
RegisterOnlyLaunch(bootstrap_ab_, std::move(host_ab_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_ab_result_);
EXPECT_EQ(host_ab_.get(), manager_->FindHost(&profile_a_, kTestAppIdB));
// Quitting when there's another shim should not terminate.
EXPECT_CALL(*manager_, MaybeTerminate()).Times(0);
manager_->OnAppDeactivated(&profile_a_, kTestAppIdA);
// Quitting when it's the last shim should terminate.
EXPECT_CALL(*manager_, MaybeTerminate());
manager_->OnAppDeactivated(&profile_a_, kTestAppIdB);
}
TEST_F(AppShimManagerTest, RegisterOnly) {
// For an chrome::mojom::AppShimLaunchType::kRegisterOnly, don't launch the
// app.
EXPECT_CALL(*delegate_, LaunchApp(_, _, _, _, _, _, _)).Times(0);
RegisterOnlyLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_TRUE(manager_->FindHost(&profile_a_, kTestAppIdA));
// Close the shim, removing the entry in the map.
manager_->OnShimProcessDisconnected(host_aa_.get());
EXPECT_FALSE(manager_->FindHost(&profile_a_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, DontCreateHost) {
delegate_->SetAppCanCreateHost(false);
// The app should be launched.
EXPECT_CALL(*delegate_, LaunchApp(_, _, _, _, _, _, _)).Times(1);
NormalLaunch(bootstrap_ab_, std::move(host_ab_unique_));
// But the bootstrap should be closed.
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccessAndDisconnect,
*bootstrap_ab_result_);
// And we should create no host.
EXPECT_FALSE(manager_->FindHost(&profile_a_, kTestAppIdB));
}
TEST_F(AppShimManagerTest, NotificationAction) {
class AppShimObserver : public AppShimManager::AppShimObserver {
public:
void OnShimProcessConnectedAndAllLaunchesDone(
base::ProcessId pid,
chrome::mojom::AppShimLaunchResult result) override {
launch_result_.SetValue(result);
}
bool OnNotificationAction(
mac_notifications::mojom::NotificationActionInfoPtr& info) override {
notification_action_.SetValue(std::move(info));
return false;
}
base::test::TestFuture<chrome::mojom::AppShimLaunchResult> launch_result_;
base::test::TestFuture<mac_notifications::mojom::NotificationActionInfoPtr>
notification_action_;
};
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// Use SetAppCanCreateHost to simulate the case where there isn't already a
// loaded profile.
delegate_->SetAppCanCreateHost(false);
AppShimObserver observer;
manager_->SetAppShimObserverForTesting(&observer);
// Create a test notification action.
auto profile_identifier = mac_notifications::mojom::ProfileIdentifier::New(
profile_a_.GetBaseName().AsUTF8Unsafe(), /*incognito=*/false);
auto notification_identifier =
mac_notifications::mojom::NotificationIdentifier::New(
"notificaiton-id", std::move(profile_identifier));
auto notification = mac_notifications::mojom::NotificationActionInfo::New();
notification->meta = mac_notifications::mojom::NotificationMetadata::New(
std::move(notification_identifier), /*notification_type=*/0,
/*origin_url=*/GURL("https://example.com"), /*user_data_dir=*/"");
notification->operation = NotificationOperation::kClick;
notification->button_index = -1;
// For an chrome::mojom::AppShimLaunchType::kNotificationAction, don't launch
// the app.
EXPECT_CALL(*delegate_, LaunchApp(_, _, _, _, _, _, _)).Times(0);
NotificationActionLaunch(bootstrap_aa_, std::move(host_aa_unique_),
std::move(notification));
// Should not have a result yet since the notification action hasn't been
// handled yet.
EXPECT_FALSE(bootstrap_aa_result_.has_value());
EXPECT_FALSE(observer.launch_result_.IsReady());
EXPECT_FALSE(observer.notification_action_.IsReady());
// Wait for the notification action to be handled.
ASSERT_TRUE(observer.notification_action_.Wait());
EXPECT_FALSE(observer.launch_result_.IsReady());
// Which should now allow to launch to finish.
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccessAndDisconnect,
observer.launch_result_.Get());
ASSERT_TRUE(bootstrap_aa_result_.has_value());
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccessAndDisconnect,
*bootstrap_aa_result_);
EXPECT_FALSE(manager_->FindHost(&profile_a_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, LoadProfile) {
// If the profile is not loaded when an OnShimProcessConnected arrives, return
// false and load the profile asynchronously. Launch the app when the profile
// is ready.
EXPECT_CALL(*manager_, ProfileForPath(profile_path_a_))
.WillOnce(Return(static_cast<Profile*>(nullptr)))
.WillRepeatedly(Return(&profile_a_));
NormalLaunch(bootstrap_aa_, std::move(host_aa_unique_));
EXPECT_FALSE(manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->RunLoadProfileCallback(profile_path_a_, &profile_a_);
EXPECT_TRUE(manager_->FindHost(&profile_a_, kTestAppIdA));
}
// Tests that calls to OnShimFocus, OnShimHide correctly handle a null extension
// being provided by the extension system.
TEST_F(AppShimManagerTest, ExtensionUninstalled) {
LaunchAndActivate(bootstrap_aa_, std::move(host_aa_unique_), &profile_a_);
ShimNormalFocus(host_aa_.get());
EXPECT_NE(nullptr, host_aa_.get());
// Set up the mock to return a null extension, as if it were uninstalled.
EXPECT_CALL(*delegate_, AppIsInstalled(&profile_a_, kTestAppIdA))
.WillRepeatedly(Return(false));
// Trying to focus will do nothing -- the shim will have to be closed by
// the user manually.
ShimNormalFocus(host_aa_.get());
EXPECT_NE(nullptr, host_aa_.get());
}
TEST_F(AppShimManagerTest, PreExistingHost) {
// Create a host for our profile.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal))
.Times(1);
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_FALSE(host_aa_->did_connect_to_host());
// Launch the app for this host. It should find the pre-existing host, and the
// pre-existing host's launch result should be set.
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
EXPECT_FALSE(host_aa_->did_connect_to_host());
DoShimLaunch(bootstrap_aa_, nullptr,
chrome::mojom::AppShimLaunchType::kRegisterOnly,
std::vector<base::FilePath>(), std::vector<GURL>(),
chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_TRUE(host_aa_->did_connect_to_host());
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Try to launch the app again. It should fail to launch, and the previous
// profile should remain.
DoShimLaunch(bootstrap_aa_duplicate_, nullptr,
chrome::mojom::AppShimLaunchType::kRegisterOnly,
std::vector<base::FilePath>(), std::vector<GURL>(),
chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_TRUE(host_aa_->did_connect_to_host());
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kDuplicateHost,
*bootstrap_aa_duplicate_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, MultiProfile) {
// Test with a bookmark app (host is shared).
{
// Create a host for profile A.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_FALSE(host_aa_->did_connect_to_host());
// Ensure that profile B has the same host.
manager_->SetHostForCreate(std::move(host_ba_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_b_, kTestAppIdA));
manager_->OnAppActivated(&profile_b_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_b_, kTestAppIdA));
EXPECT_FALSE(host_aa_->did_connect_to_host());
}
// Test with a non-bookmark app (host is not shared).
{
// Create a host for profile A.
manager_->SetHostForCreate(std::move(host_ab_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdB));
manager_->OnAppActivated(&profile_a_, kTestAppIdB);
EXPECT_EQ(host_ab_.get(), manager_->FindHost(&profile_a_, kTestAppIdB));
EXPECT_FALSE(host_ab_->did_connect_to_host());
// Ensure that profile B has the same host.
manager_->SetHostForCreate(std::move(host_bb_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_b_, kTestAppIdB));
manager_->OnAppActivated(&profile_b_, kTestAppIdB);
EXPECT_EQ(host_bb_.get(), manager_->FindHost(&profile_b_, kTestAppIdB));
EXPECT_FALSE(host_bb_->did_connect_to_host());
}
}
TEST_F(AppShimManagerTest, MultiProfileShimLaunch) {
manager_->SetHostForCreate(std::move(host_aa_unique_));
ShimLaunchedCallback launched_callback;
delegate_->SetCaptureShimLaunchedCallback(&launched_callback);
ShimTerminatedCallback terminated_callback;
delegate_->SetCaptureShimTerminatedCallback(&terminated_callback);
// Launch the app for profile A. This should trigger a shim launch request.
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_FALSE(host_aa_->did_connect_to_host());
// Launch the app for profile B. This should not cause a shim launch request.
EXPECT_CALL(*delegate_, DoLaunchShim(_, _, _, _)).Times(0);
manager_->OnAppActivated(&profile_b_, kTestAppIdA);
// Indicate the profile A that its launch succeeded.
EXPECT_TRUE(launched_callback);
EXPECT_TRUE(terminated_callback);
std::move(launched_callback).Run(base::Process(5));
EXPECT_FALSE(launched_callback);
EXPECT_TRUE(terminated_callback);
}
TEST_F(AppShimManagerTest, MultiProfileSelectMenu) {
EXPECT_CALL(*delegate_, ShowAppWindows(_, _)).WillRepeatedly(Return(false));
manager_->SetHostForCreate(std::move(host_aa_unique_));
ShimLaunchedCallback launched_callback;
delegate_->SetCaptureShimLaunchedCallback(&launched_callback);
ShimTerminatedCallback terminated_callback;
delegate_->SetCaptureShimTerminatedCallback(&terminated_callback);
// Launch the app for profile A. This should trigger a shim launch request.
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_FALSE(host_aa_->did_connect_to_host());
// Indicate the profile A that its launch succeeded.
EXPECT_TRUE(launched_callback);
EXPECT_TRUE(terminated_callback);
std::move(launched_callback).Run(base::Process(5));
EXPECT_FALSE(launched_callback);
EXPECT_TRUE(terminated_callback);
// Select profile B from the menu. This should request that the app be
// launched.
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _));
host_aa_->ProfileSelectedFromMenu(profile_path_b_);
EXPECT_CALL(*delegate_, DoLaunchShim(_, _, _, _)).Times(0);
manager_->OnAppActivated(&profile_b_, kTestAppIdA);
// Select profile A and B from the menu -- this should not request a launch,
// because the profiles are already enabled.
EXPECT_CALL(*delegate_, ShowAppWindows(_, _)).WillRepeatedly(Return(true));
EXPECT_CALL(*delegate_,
LaunchApp(_, _, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
host_aa_->ProfileSelectedFromMenu(profile_path_a_);
host_aa_->ProfileSelectedFromMenu(profile_path_b_);
}
namespace {
// A helper that records when Show is called on a BrowserWindow to verify
// activation of existing browser windows.
class TestBrowserWindowShow : public TestBrowserWindow {
public:
void Show() override { did_show = true; }
bool did_show = false;
};
} // namespace
TEST_F(AppShimManagerTest, MultiProfileSelectMenu_ShowsBrowser) {
EXPECT_CALL(*delegate_, ShowAppWindows(_, _)).WillRepeatedly(Return(false));
manager_->SetHostForCreate(std::move(host_aa_unique_));
ShimLaunchedCallback launched_callback;
delegate_->SetCaptureShimLaunchedCallback(&launched_callback);
ShimTerminatedCallback terminated_callback;
delegate_->SetCaptureShimTerminatedCallback(&terminated_callback);
// Launch the app for profile A. This should trigger a shim launch request.
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
EXPECT_FALSE(host_aa_->did_connect_to_host());
// Indicate the profile A that its launch succeeded.
EXPECT_TRUE(launched_callback);
EXPECT_TRUE(terminated_callback);
std::move(launched_callback).Run(base::Process(5));
EXPECT_FALSE(launched_callback);
EXPECT_TRUE(terminated_callback);
// Notify manager that a new browser has been associated with the app.
auto browser_window_a = std::make_unique<TestBrowserWindowShow>();
std::string app_name = web_app::GenerateApplicationNameFromAppId(kTestAppIdA);
Browser::CreateParams params_a = Browser::CreateParams::CreateForApp(
app_name, true, browser_window_a->GetBounds(), &profile_a_, true);
params_a.window = browser_window_a.get();
auto browser_a = std::unique_ptr<Browser>(Browser::Create(params_a));
manager_->OnBrowserAdded(browser_a.get());
// Select profile B from the menu. This should request that the app be
// launched.
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _));
host_aa_->ProfileSelectedFromMenu(profile_path_b_);
EXPECT_CALL(*delegate_, DoLaunchShim(_, _, _, _)).Times(0);
manager_->OnAppActivated(&profile_b_, kTestAppIdA);
// Notify manager that a new browser has been associated with the app.
auto browser_window_b = std::make_unique<TestBrowserWindowShow>();
Browser::CreateParams params_b = Browser::CreateParams::CreateForApp(
app_name, true, browser_window_b->GetBounds(), &profile_b_, true);
params_b.window = browser_window_b.get();
auto browser_b = std::unique_ptr<Browser>(Browser::Create(params_b));
manager_->OnBrowserAdded(browser_b.get());
EXPECT_FALSE(browser_window_a->did_show);
EXPECT_FALSE(browser_window_b->did_show);
// Select profile A and B from the menu -- this should not request a launch,
// because the profiles are already enabled.
EXPECT_CALL(*delegate_, ShowAppWindows(_, _)).WillRepeatedly(Return(false));
EXPECT_CALL(*delegate_,
LaunchApp(_, _, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
host_aa_->ProfileSelectedFromMenu(profile_path_a_);
EXPECT_TRUE(browser_window_a->did_show);
EXPECT_FALSE(browser_window_b->did_show);
browser_window_a->did_show = false;
host_aa_->ProfileSelectedFromMenu(profile_path_b_);
EXPECT_FALSE(browser_window_a->did_show);
EXPECT_TRUE(browser_window_b->did_show);
}
TEST_F(AppShimManagerTest, ProfileMenuOneProfile) {
{
auto item_a = chrome::mojom::ProfileMenuItem::New();
item_a->profile_path = profile_path_a_;
item_a->menu_index = 999;
std::vector<chrome::mojom::ProfileMenuItemPtr> items;
items.push_back(std::move(item_a));
manager_->SetProfileMenuItems(std::move(items));
}
// Set this app to be installed for profile A.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
// When the app activates, a host is created. This will trigger building
// the avatar menu.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Launch the shim.
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
RegisterOnlyLaunch(bootstrap_aa_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_aa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
const auto& menu_items = host_aa_->test_app_shim_->profile_menu_items_;
// We should have no menu items, because there is only one installed profile.
EXPECT_TRUE(menu_items.empty());
// Add profile B to the avatar menu and call the avatar menu observer update
// method.
{
auto item_a = chrome::mojom::ProfileMenuItem::New();
item_a->profile_path = profile_path_a_;
item_a->menu_index = 999;
auto item_b = chrome::mojom::ProfileMenuItem::New();
item_b->profile_path = profile_path_b_;
item_b->menu_index = 111;
std::vector<chrome::mojom::ProfileMenuItemPtr> items;
items.push_back(std::move(item_a));
items.push_back(std::move(item_b));
manager_->SetProfileMenuItems(std::move(items));
}
// We should still only have no menu items, because the app is not installed
// for multiple profiles.
EXPECT_TRUE(menu_items.empty());
// Now install for profile B.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_b_);
manager_->OnAppActivated(&profile_b_, kTestAppIdA);
EXPECT_EQ(menu_items.size(), 2u);
EXPECT_EQ(menu_items[0]->profile_path, profile_path_b_);
EXPECT_EQ(menu_items[1]->profile_path, profile_path_a_);
}
TEST_F(AppShimManagerTest, FindProfileFromBadProfile) {
// Set this app to be installed for profile A and B.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_b_);
// Set the app to be last-active on profile A.
std::set<base::FilePath> last_active_profile_paths;
last_active_profile_paths.insert(profile_path_a_);
AppShimRegistry::Get()->SaveLastActiveProfilesForApp(
kTestAppIdA, last_active_profile_paths);
// Launch the shim requesting profile C.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(1);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
EXPECT_CALL(*delegate_, EnableExtension(&profile_c_, kTestAppIdA, _))
.WillOnce(RunOnceCallback<2>());
NormalLaunch(bootstrap_ca_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_ca_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, FindProfileFromNoProfile) {
// Set this app to be installed for profile A.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
// Launch the shim without specifying a profile.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(1);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
NormalLaunch(bootstrap_xa_, nullptr);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_xa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, FindProfileFromFilePaths) {
// Set this app to be install for profile A, B and C.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_b_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_c_);
// Configure different file handlers in each of the three profiles.
AppShimRegistry::Get()->SaveFileHandlersForAppAndProfile(
kTestAppIdA, profile_path_a_, {".md"}, {});
AppShimRegistry::Get()->SaveFileHandlersForAppAndProfile(
kTestAppIdA, profile_path_b_, {".txt", ".csv"}, {});
AppShimRegistry::Get()->SaveFileHandlersForAppAndProfile(
kTestAppIdA, profile_path_c_, {".txt"}, {});
// Launch the shim passing in several files.
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(1);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_c_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
DoShimLaunch(bootstrap_xa_, std::move(host_ba_unique_),
chrome::mojom::AppShimLaunchType::kNormal,
{base::FilePath("/foo/bar/test.txt"),
base::FilePath("/home/test/data.csv"),
base::FilePath("/data/README.md")},
{}, chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_xa_result_);
EXPECT_EQ(host_ba_.get(), manager_->FindHost(&profile_b_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, FindProfileFromFileURL) {
// Set this app to be install for profile A, B and C.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_b_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_c_);
// Configure different file handlers in each of the three profiles.
AppShimRegistry::Get()->SaveFileHandlersForAppAndProfile(
kTestAppIdA, profile_path_a_, {".md"}, {});
AppShimRegistry::Get()->SaveFileHandlersForAppAndProfile(
kTestAppIdA, profile_path_b_, {".txt", ".csv"}, {});
AppShimRegistry::Get()->SaveFileHandlersForAppAndProfile(
kTestAppIdA, profile_path_c_, {".txt"}, {});
// Launch the shim passing in a file URL.
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(1);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_c_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
DoShimLaunch(bootstrap_xa_, std::move(host_aa_unique_),
chrome::mojom::AppShimLaunchType::kNormal, {},
{GURL("file:///data/README.md")},
chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_xa_result_);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, FindProfileFromURL) {
// Set this app to be install for profile A, B and C.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_b_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_c_);
// Configure different protocol handlers in each of the three profiles.
AppShimRegistry::Get()->SaveProtocolHandlersForAppAndProfile(
kTestAppIdA, profile_path_a_, {"web+music"});
AppShimRegistry::Get()->SaveProtocolHandlersForAppAndProfile(
kTestAppIdA, profile_path_b_, {"web+jngl"});
AppShimRegistry::Get()->SaveProtocolHandlersForAppAndProfile(
kTestAppIdA, profile_path_c_, {"mailto"});
// Launch the shim passing in a URL to be handled in profile B.
EXPECT_CALL(*delegate_,
LaunchApp(&profile_a_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_b_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(1);
EXPECT_CALL(*delegate_,
LaunchApp(&profile_c_, kTestAppIdA, _, _, _,
chrome::mojom::AppShimLoginItemRestoreState::kNone, _))
.Times(0);
DoShimLaunch(bootstrap_xa_, std::move(host_ba_unique_),
chrome::mojom::AppShimLaunchType::kNormal, {},
{GURL("web+jngl://foo/bar")},
chrome::mojom::AppShimLoginItemRestoreState::kNone);
EXPECT_EQ(chrome::mojom::AppShimLaunchResult::kSuccess,
*bootstrap_xa_result_);
EXPECT_EQ(host_ba_.get(), manager_->FindHost(&profile_b_, kTestAppIdA));
}
TEST_F(AppShimManagerTest, UpdateAppBadge) {
// Set this app to be installed for profile A and B.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_b_);
// Activate the app for profile_a_ and profile_b_
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->SetHostForCreate(std::move(host_ba_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_b_, kTestAppIdA));
manager_->OnAppActivated(&profile_b_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_b_, kTestAppIdA));
// And update the badge in either profile, verifying that the combined value
// is reflected.
EXPECT_EQ("", host_aa_->test_app_shim_->badge_label_);
manager_->UpdateAppBadge(&profile_a_, kTestAppIdA,
badging::BadgeManager::BadgeValue(4));
EXPECT_EQ("4", host_aa_->test_app_shim_->badge_label_);
manager_->UpdateAppBadge(&profile_b_, kTestAppIdA,
badging::BadgeManager::BadgeValue(3));
EXPECT_EQ("7", host_aa_->test_app_shim_->badge_label_);
manager_->UpdateAppBadge(&profile_a_, kTestAppIdA,
badging::BadgeManager::BadgeValue());
EXPECT_EQ("3", host_aa_->test_app_shim_->badge_label_);
manager_->UpdateAppBadge(&profile_b_, kTestAppIdA,
badging::BadgeManager::BadgeValue());
EXPECT_EQ("•", host_aa_->test_app_shim_->badge_label_);
manager_->UpdateAppBadge(&profile_a_, kTestAppIdA, std::nullopt);
EXPECT_EQ("•", host_aa_->test_app_shim_->badge_label_);
manager_->UpdateAppBadge(&profile_b_, kTestAppIdA, std::nullopt);
EXPECT_EQ("", host_aa_->test_app_shim_->badge_label_);
}
TEST_F(AppShimManagerTest, UpdateApplicationDockMenu) {
// Set this app to be installed for profile A and B.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_b_);
struct DockMenuItems {
std::u16string name;
GURL url;
};
DockMenuItems menu_items_profile_a[] = {
{u"dock_menu_item_a_1", GURL(".")},
{u"dock_menu_item_a_2", GURL("/settings")},
{u"dock_menu_item_a_3", GURL("https://anothersite.com")},
};
const size_t kNumMenuItemsForProfileA = std::size(menu_items_profile_a);
DockMenuItems menu_items_profile_b[] = {
{u"dock_menu_item_b_1", GURL("/about")},
{u"dock_menu_item_b_2", GURL("/another-link")},
};
const size_t kNumMenuItemsForProfileB = std::size(menu_items_profile_b);
// Lambda to help with creation of application dock menu items.
auto MakeDockMenuItems = [](DockMenuItems* menu_items,
size_t menu_items_size) {
std::vector<chrome::mojom::ApplicationDockMenuItemPtr> mock_dock_menu_items;
for (size_t i = 0; i < menu_items_size; i++) {
auto dock_menu_item = chrome::mojom::ApplicationDockMenuItem::New();
dock_menu_item->name = menu_items[i].name;
dock_menu_item->url = menu_items[i].url;
mock_dock_menu_items.push_back(std::move(dock_menu_item));
}
return mock_dock_menu_items;
};
auto ValidateDockMenuItems = [&](DockMenuItems* expected_menu_items,
size_t expected_menu_items_size) {
const auto& dock_menu_items = host_aa_->test_app_shim_->dock_menu_items_;
EXPECT_EQ(expected_menu_items_size, dock_menu_items.size());
for (size_t i = 0; i < dock_menu_items.size(); i++) {
EXPECT_EQ(expected_menu_items[i].name, dock_menu_items[i]->name);
EXPECT_EQ(expected_menu_items[i].url, dock_menu_items[i]->url);
}
};
// Activate the app for profile_a_ and profile_b_
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
manager_->SetHostForCreate(std::move(host_ba_unique_));
EXPECT_EQ(nullptr, manager_->FindHost(&profile_b_, kTestAppIdA));
manager_->OnAppActivated(&profile_b_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_b_, kTestAppIdA));
// Validate no application dock menu items have been set yet.
ValidateDockMenuItems(nullptr, 0);
// Create browser objects that can be passed via OnBrowserSetLastActive.
std::string app_name = web_app::GenerateApplicationNameFromAppId(kTestAppIdA);
std::unique_ptr<Browser> browser_profile_a, browser_profile_b;
{
auto browser_window = std::make_unique<TestBrowserWindow>();
Browser::CreateParams params = Browser::CreateParams::CreateForApp(
app_name, true, browser_window->GetBounds(), &profile_a_, true);
params.window = browser_window.get();
browser_profile_a = std::unique_ptr<Browser>(Browser::Create(params));
}
{
auto browser_window = std::make_unique<TestBrowserWindow>();
Browser::CreateParams params = Browser::CreateParams::CreateForApp(
app_name, true, browser_window->GetBounds(), &profile_b_, true);
params.window = browser_window.get();
browser_profile_b = std::unique_ptr<Browser>(Browser::Create(params));
}
// Set profile A browser as last active, and validate the application dock
// menu items.
EXPECT_CALL(*delegate_,
GetAppShortcutsMenuItemInfos(&profile_a_, kTestAppIdA))
.WillOnce(Return(testing::ByMove(
MakeDockMenuItems(menu_items_profile_a, kNumMenuItemsForProfileA))));
manager_->OnBrowserSetLastActive(browser_profile_a.get());
ValidateDockMenuItems(menu_items_profile_a, kNumMenuItemsForProfileA);
// Set profile B browser as last active, and validate the application dock
// menu items.
EXPECT_CALL(*delegate_,
GetAppShortcutsMenuItemInfos(&profile_b_, kTestAppIdA))
.WillOnce(Return(testing::ByMove(
MakeDockMenuItems(menu_items_profile_b, kNumMenuItemsForProfileB))));
manager_->OnBrowserSetLastActive(browser_profile_b.get());
ValidateDockMenuItems(menu_items_profile_b, kNumMenuItemsForProfileB);
}
TEST_F(AppShimManagerTest,
BuildAppShimRequirementStringFromFrameworkRequirementStringTest) {
EXPECT_TRUE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("identifier \"com.google.Chrome.framework\" and certificate "
"leaf = H\"c9a99324ca3fcb23dbcc36bd5fd4f9753305130a\"")));
EXPECT_TRUE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("identifier \"com.google.Chrome.framework\" and certificate "
"leaf[subject.OU] = \"42HXZ8M8AV\"")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR(
"cdhash H\"daa66a31aeb85125bd2459bebf548b2dff5ee83b\" or cdhash "
"H\"a8e5300bf9223510fc5b107b23de0d12f419acac\"")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("identifier \"com.google.Chrome.framework\"")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("identifier")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("malformed")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("\"\"\"")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("\"\"")));
EXPECT_FALSE(
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("\"")));
// Crafted to pass all our requirement checks but fail
// SecRequirementCreateWithString().
base::apple::ScopedCFTypeRef<CFStringRef> requirement_string =
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("identifier \"com.google.Chrome.framework\" and fail here"));
EXPECT_TRUE(requirement_string);
EXPECT_FALSE(apps::RequirementFromString(requirement_string.get()));
// Missing quote in the post "identifier" portion which is caught by
// SecRequirementCreateWithString().
requirement_string =
manager_->BuildAppShimRequirementStringFromFrameworkRequirementString(
CFSTR("identifier \"com.google.Chrome.framework\" and certificate "
"leaf = Hc9a99324ca3fcb23dbcc36bd5fd4f9753305130a\""));
EXPECT_TRUE(requirement_string);
EXPECT_FALSE(apps::RequirementFromString(requirement_string.get()));
CFStringRef framework_req_string = CFSTR(
"identifier \"com.google.Chrome.framework\" and anchor "
"apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* "
"exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] "
"/* exists */ and certificate leaf[subject.OU] = EQHXZ8M8AV");
base::apple::ScopedCFTypeRef<SecRequirementRef> got_req(
apps::RequirementFromString(
manager_
->BuildAppShimRequirementStringFromFrameworkRequirementString(
framework_req_string)
.get()));
ASSERT_TRUE(got_req);
base::apple::ScopedCFTypeRef<CFStringRef> got_req_string;
ASSERT_EQ(SecRequirementCopyString(got_req.get(), kSecCSDefaultFlags,
got_req_string.InitializeInto()),
errSecSuccess);
CFStringRef want_req_string = CFSTR(
"identifier \"app_mode_loader\" and anchor "
"apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* "
"exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] "
"/* exists */ and certificate leaf[subject.OU] = EQHXZ8M8AV");
EXPECT_EQ(base::SysCFStringRefToUTF8(got_req_string.get()),
base::SysCFStringRefToUTF8(want_req_string));
}
TEST_F(AppShimManagerTest, LaunchNotificationProviderWithAppRunning) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
// Launch the app shim.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Connect to its notification provider.
mojo::Remote<mac_notifications::mojom::MacNotificationProvider> provider =
manager_->LaunchNotificationProvider(kTestAppIdA);
EXPECT_TRUE(provider.is_bound());
EXPECT_TRUE(
host_aa_->test_app_shim_->notification_provider_receiver_.is_bound());
provider.FlushForTesting();
EXPECT_TRUE(provider.is_connected());
}
TEST_F(AppShimManagerTest, LaunchNotificationProviderWithoutAppRunning) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
EXPECT_CALL(*manager_, ProfileForBackgroundShimLaunch(kTestAppIdA))
.WillOnce(Return(&profile_a_));
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kBackground));
mojo::Remote<mac_notifications::mojom::MacNotificationProvider> provider =
manager_->LaunchNotificationProvider(kTestAppIdA);
EXPECT_TRUE(provider.is_bound());
EXPECT_TRUE(
host_aa_->test_app_shim_->notification_provider_receiver_.is_bound());
provider.FlushForTesting();
EXPECT_TRUE(provider.is_connected());
}
TEST_F(AppShimManagerTest, LaunchNotificationProviderWithAppNotInstalled) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
EXPECT_CALL(*manager_, ProfileForBackgroundShimLaunch(kTestAppIdA))
.WillOnce(Return(nullptr));
mojo::Remote<mac_notifications::mojom::MacNotificationProvider> provider =
manager_->LaunchNotificationProvider(kTestAppIdA);
// AppShimManager binds `provider` to a dummy implementation if an app can't
// be found, since notifications code expects to always get a bound provider.
EXPECT_TRUE(provider.is_bound());
provider.FlushForTesting();
EXPECT_TRUE(provider.is_connected());
// Attempting to bind a notification service on the dummy provider should
// immediately disconnect.
mojo::Remote<mac_notifications::mojom::MacNotificationService> service;
mojo::PendingReceiver<mac_notifications::mojom::MacNotificationActionHandler>
handler;
provider->BindNotificationService(service.BindNewPipeAndPassReceiver(),
handler.InitWithNewPipeAndPassRemote());
EXPECT_TRUE(service.is_connected());
service.FlushForTesting();
EXPECT_FALSE(service.is_connected());
EXPECT_TRUE(provider.is_connected());
}
TEST_F(AppShimManagerTest, RequestNotificationPermissionWithAppRunning) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
// Launch the app shim.
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
EXPECT_EQ(host_aa_.get(), manager_->FindHost(&profile_a_, kTestAppIdA));
// Trigger a notification permission request.
base::test::TestFuture<mac_notifications::mojom::RequestPermissionResult>
result;
manager_->ShowNotificationPermissionRequest(kTestAppIdA,
result.GetCallback());
EXPECT_TRUE(host_aa_->test_app_shim_
->request_notification_permission_callback_.Wait());
host_aa_->test_app_shim_->request_notification_permission_callback_.Take()
.Run(mac_notifications::mojom::RequestPermissionResult::
kPermissionPreviouslyGranted);
EXPECT_EQ(mac_notifications::mojom::RequestPermissionResult::
kPermissionPreviouslyGranted,
result.Get());
}
TEST_F(AppShimManagerTest, RequestNotificationPermissionWithoutAppRunning) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
EXPECT_CALL(*manager_, ProfileForBackgroundShimLaunch(kTestAppIdA))
.WillOnce(Return(&profile_a_));
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kBackground));
// Trigger a notification permission request.
base::test::TestFuture<mac_notifications::mojom::RequestPermissionResult>
result;
manager_->ShowNotificationPermissionRequest(kTestAppIdA,
result.GetCallback());
EXPECT_TRUE(host_aa_->test_app_shim_
->request_notification_permission_callback_.Wait());
host_aa_->test_app_shim_->request_notification_permission_callback_.Take()
.Run(mac_notifications::mojom::RequestPermissionResult::
kPermissionPreviouslyDenied);
EXPECT_EQ(mac_notifications::mojom::RequestPermissionResult::
kPermissionPreviouslyDenied,
result.Get());
}
TEST_F(AppShimManagerTest,
RequestNotificationPermissionWithoutAppRunningAndBrowserClosing) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
EXPECT_CALL(*manager_, ProfileForBackgroundShimLaunch(kTestAppIdA))
.WillOnce(Return(&profile_a_));
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kBackground));
// Trigger a notification permission request.
base::test::TestFuture<mac_notifications::mojom::RequestPermissionResult>
result;
manager_->ShowNotificationPermissionRequest(kTestAppIdA,
result.GetCallback());
EXPECT_TRUE(host_aa_->test_app_shim_
->request_notification_permission_callback_.Wait());
// Pretend the last browser for this app/profile was just closed, and the
// profile has been unloaded as a result of that.
manager_->OnAppDeactivated(&profile_a_, kTestAppIdA);
EXPECT_CALL(*manager_, ProfileForPath(profile_path_a_))
.WillRepeatedly(Return(nullptr));
EXPECT_CALL(*delegate_, AppIsInstalled(nullptr, kTestAppIdA))
.WillRepeatedly(Return(false));
// Now have the app shim connect to the browser process.
RegisterOnlyLaunch(bootstrap_aa_, nullptr);
host_aa_->test_app_shim_->request_notification_permission_callback_.Take()
.Run(mac_notifications::mojom::RequestPermissionResult::
kPermissionPreviouslyDenied);
EXPECT_EQ(mac_notifications::mojom::RequestPermissionResult::
kPermissionPreviouslyDenied,
result.Get());
}
TEST_F(AppShimManagerTest,
AppShimFailToConnectForNotificationPermissionAfterBrowserClosed) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
EXPECT_CALL(*manager_, ProfileForBackgroundShimLaunch(kTestAppIdA))
.WillOnce(Return(&profile_a_));
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kBackground));
// Capture the terminated callback so we can simulate the app shim failing
// to launch.
ShimTerminatedCallback terminated_callback;
delegate_->SetCaptureShimTerminatedCallback(&terminated_callback);
// Trigger a notification permission request.
base::test::TestFuture<mac_notifications::mojom::RequestPermissionResult>
result;
manager_->ShowNotificationPermissionRequest(kTestAppIdA,
result.GetCallback());
EXPECT_TRUE(host_aa_->test_app_shim_
->request_notification_permission_callback_.Wait());
// Pretend the last browser for this app/profile was just closed, and the
// profile has been unloaded as a result of that.
manager_->OnAppDeactivated(&profile_a_, kTestAppIdA);
EXPECT_CALL(*manager_, ProfileForPath(profile_path_a_))
.WillRepeatedly(Return(nullptr));
EXPECT_CALL(*delegate_, AppIsInstalled(nullptr, kTestAppIdA))
.WillRepeatedly(Return(false));
// Report that the process terminated.
ASSERT_TRUE(terminated_callback);
std::move(terminated_callback).Run();
}
TEST_F(AppShimManagerTest,
RequestNotificationPermissionWithAppShimFailingToLaunch) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// This app is installed for profile A throughout this test.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
EXPECT_CALL(*manager_, ProfileForBackgroundShimLaunch(kTestAppIdA))
.WillOnce(Return(&profile_a_));
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kBackground));
// Trigger a notification permission request.
base::test::TestFuture<mac_notifications::mojom::RequestPermissionResult>
result;
manager_->ShowNotificationPermissionRequest(kTestAppIdA,
result.GetCallback());
EXPECT_TRUE(host_aa_->test_app_shim_
->request_notification_permission_callback_.Wait());
// Simulate the app shim failing to launch (or otherwise terminating) by
// dropping the callback.
host_aa_->test_app_shim_->request_notification_permission_callback_.Take()
.Reset();
EXPECT_EQ(mac_notifications::mojom::RequestPermissionResult::kRequestFailed,
result.Get());
}
TEST_F(AppShimManagerTest, RequestNotificationPermissionWithAppNotInstalled) {
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
EXPECT_CALL(*manager_, ProfileForBackgroundShimLaunch(kTestAppIdA))
.WillOnce(Return(nullptr));
// Trigger a notification permission request.
base::test::TestFuture<mac_notifications::mojom::RequestPermissionResult>
result;
manager_->ShowNotificationPermissionRequest(kTestAppIdA,
result.GetCallback());
EXPECT_EQ(mac_notifications::mojom::RequestPermissionResult::kRequestFailed,
result.Get());
}
TEST_F(AppShimManagerTest, CachedNotificationPermissionStatus) {
using PermissionStatus = mac_notifications::mojom::PermissionStatus;
scoped_feature_list_.InitWithFeatures(
{features::kAppShimNotificationAttribution}, {});
// Create and launch shim for app A in profile A.
AppShimRegistry::Get()->OnAppInstalledForProfile(kTestAppIdA,
profile_path_a_);
manager_->SetHostForCreate(std::move(host_aa_unique_));
EXPECT_CALL(*delegate_,
DoLaunchShim(&profile_a_, kTestAppIdA,
web_app::LaunchShimUpdateBehavior::kDoNotRecreate,
web_app::ShimLaunchMode::kNormal));
manager_->OnAppActivated(&profile_a_, kTestAppIdA);
// Initial cached status should be "not determined".
EXPECT_EQ(PermissionStatus::kNotDetermined,
AppShimRegistry::Get()->GetNotificationPermissionStatusForApp(
kTestAppIdA));
// Trigger updates to the notification status.
base::test::TestFuture<const std::string&> app_changed;
auto app_changed_registration =
AppShimRegistry::Get()->RegisterAppChangedCallback(
app_changed.GetRepeatingCallback());
for (auto status :
{PermissionStatus::kGranted, PermissionStatus::kNotDetermined,
PermissionStatus::kPromptPending, PermissionStatus::kDenied}) {
host_aa_->NotificationPermissionStatusChanged(status);
EXPECT_EQ(kTestAppIdA, app_changed.Take());
EXPECT_EQ(status,
AppShimRegistry::Get()->GetNotificationPermissionStatusForApp(
kTestAppIdA));
}
}
} // namespace apps