// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/os_integration/mac/web_app_shortcut_creator.h"
#import <Cocoa/Cocoa.h>
#include <errno.h>
#include <stddef.h>
#include <sys/xattr.h>
#include <memory>
#include <optional>
#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_path_override.h"
#include "chrome/browser/web_applications/os_integration/mac/apps_folder_support.h"
#include "chrome/browser/web_applications/os_integration/mac/web_app_auto_login_util.h"
#include "chrome/browser/web_applications/os_integration/mac/web_app_shortcut_mac.h"
#include "chrome/browser/web_applications/os_integration/web_app_shortcut.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#import "chrome/common/mac/app_mode_common.h"
#include "chrome/grit/theme_resources.h"
#include "components/version_info/version_info.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
using ::testing::_;
using ::testing::Return;
using ::testing::NiceMock;
namespace web_app {
namespace {
const char kFakeChromeBundleId[] = "fake.cfbundleidentifier";
class WebAppShortcutCreatorMock : public WebAppShortcutCreator {
public:
WebAppShortcutCreatorMock(const base::FilePath& app_data_dir,
const ShortcutInfo* shortcut_info)
: WebAppShortcutCreator(app_data_dir,
GetChromeAppsFolder(),
shortcut_info,
web_app::UseAdHocSigningForWebAppShims()) {}
MOCK_CONST_METHOD0(GetAppBundlesByIdUnsorted, std::vector<base::FilePath>());
MOCK_CONST_METHOD1(RevealAppShimInFinder, void(const base::FilePath&));
WebAppShortcutCreatorMock(const WebAppShortcutCreatorMock&) = delete;
WebAppShortcutCreatorMock& operator=(const WebAppShortcutCreatorMock&) =
delete;
};
class WebAppShortcutCreatorSortingMock : public WebAppShortcutCreator {
public:
WebAppShortcutCreatorSortingMock(const base::FilePath& app_data_dir,
const ShortcutInfo* shortcut_info)
: WebAppShortcutCreator(app_data_dir,
GetChromeAppsFolder(),
shortcut_info,
web_app::UseAdHocSigningForWebAppShims()) {}
MOCK_CONST_METHOD0(GetAppBundlesByIdUnsorted, std::vector<base::FilePath>());
WebAppShortcutCreatorSortingMock(const WebAppShortcutCreatorSortingMock&) =
delete;
WebAppShortcutCreatorSortingMock& operator=(
const WebAppShortcutCreatorSortingMock&) = delete;
};
class WebAppAutoLoginUtilMock : public WebAppAutoLoginUtil {
public:
WebAppAutoLoginUtilMock() = default;
WebAppAutoLoginUtilMock(const WebAppAutoLoginUtilMock&) = delete;
WebAppAutoLoginUtilMock& operator=(const WebAppAutoLoginUtilMock&) = delete;
void AddToLoginItems(const base::FilePath& app_bundle_path,
bool hide_on_startup) override {
EXPECT_TRUE(base::PathExists(app_bundle_path));
EXPECT_FALSE(hide_on_startup);
add_to_login_items_called_count_++;
}
void RemoveFromLoginItems(const base::FilePath& app_bundle_path) override {
EXPECT_TRUE(base::PathExists(app_bundle_path));
remove_from_login_items_called_count_++;
}
void ResetCounts() {
add_to_login_items_called_count_ = 0;
remove_from_login_items_called_count_ = 0;
}
int GetAddToLoginItemsCalledCount() const {
return add_to_login_items_called_count_;
}
int GetRemoveFromLoginItemsCalledCount() const {
return remove_from_login_items_called_count_;
}
private:
int add_to_login_items_called_count_ = 0;
int remove_from_login_items_called_count_ = 0;
};
std::unique_ptr<ShortcutInfo> GetShortcutInfo() {
std::unique_ptr<ShortcutInfo> info(new ShortcutInfo);
info->app_id = "appid";
info->title = u"Shortcut Title";
info->url = GURL("http://example.com/");
info->profile_path = base::FilePath("user_data_dir").Append("Profile 1");
info->profile_name = "profile name";
info->version_for_display = "stable 1.0";
info->is_multi_profile = true;
return info;
}
class WebAppShortcutCreatorTest : public testing::Test {
public:
WebAppShortcutCreatorTest(const WebAppShortcutCreatorTest&) = delete;
WebAppShortcutCreatorTest& operator=(const WebAppShortcutCreatorTest&) =
delete;
protected:
WebAppShortcutCreatorTest() = default;
void SetUp() override {
base::apple::SetBaseBundleID(kFakeChromeBundleId);
override_registration_ =
OsIntegrationTestOverrideImpl::OverrideForTesting();
destination_dir_ =
override_registration_->test_override().chrome_apps_folder();
EXPECT_TRUE(temp_user_data_dir_.CreateUniqueTempDir());
user_data_dir_ = temp_user_data_dir_.GetPath();
// Recreate the directory structure as it would be created for the
// ShortcutInfo created in the above GetShortcutInfo.
app_data_dir_ = user_data_dir_.Append("Profile 1")
.Append("Web Applications")
.Append("_crx_extensionid");
EXPECT_TRUE(base::CreateDirectory(app_data_dir_));
// When using base::PathService::Override, it calls
// base::MakeAbsoluteFilePath. On Mac this prepends "/private" to the path,
// but points to the same directory in the file system.
user_data_dir_override_.emplace(chrome::DIR_USER_DATA, user_data_dir_);
user_data_dir_ = base::MakeAbsoluteFilePath(user_data_dir_);
app_data_dir_ = base::MakeAbsoluteFilePath(app_data_dir_);
info_ = GetShortcutInfo();
fallback_shim_base_name_ = base::FilePath(
info_->profile_path.BaseName().value() + " " + info_->app_id + ".app");
shim_base_name_ = base::FilePath(base::UTF16ToUTF8(info_->title) + ".app");
shim_path_ = destination_dir_.Append(shim_base_name_);
auto_login_util_mock_ = std::make_unique<WebAppAutoLoginUtilMock>();
WebAppAutoLoginUtil::SetInstanceForTesting(auto_login_util_mock_.get());
// Make sure that the tests in this class will actually try to set the
// localized app dir name.
ResetHaveLocalizedAppDirNameForTesting();
}
void TearDown() override {
WebAppAutoLoginUtil::SetInstanceForTesting(nullptr);
override_registration_.reset();
testing::Test::TearDown();
}
// Needed by DCHECK_CURRENTLY_ON in ShortcutInfo destructor.
content::BrowserTaskEnvironment task_environment_;
base::ScopedTempDir temp_user_data_dir_;
base::FilePath app_data_dir_;
base::FilePath destination_dir_;
base::FilePath user_data_dir_;
std::optional<base::ScopedPathOverride> user_data_dir_override_;
std::unique_ptr<WebAppAutoLoginUtilMock> auto_login_util_mock_;
std::unique_ptr<ShortcutInfo> info_;
base::FilePath fallback_shim_base_name_;
base::FilePath shim_base_name_;
base::FilePath shim_path_;
std::unique_ptr<OsIntegrationTestOverrideImpl::BlockingRegistration>
override_registration_;
};
} // namespace
TEST_F(WebAppShortcutCreatorTest, CreateShortcuts) {
base::FilePath strings_file =
destination_dir_.Append(".localized").Append("en_US.strings");
// The Chrome Apps folder shouldn't be localized yet.
EXPECT_FALSE(base::PathExists(strings_file));
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_TRUE(base::PathExists(destination_dir_));
EXPECT_EQ(shim_base_name_, shortcut_creator.GetShortcutBasename());
// When a shortcut is created, the parent, "Chrome Apps" folder should become
// localized, but only once, to avoid concurrency issues in NSWorkspace. Note
// this will fail if the CreateShortcuts test is run multiple times in the
// same process, but the test runner should never do that.
EXPECT_TRUE(base::PathExists(strings_file));
// Delete it here, just to test that it is not recreated.
EXPECT_TRUE(base::DeletePathRecursively(strings_file));
auto_login_util_mock_->ResetCounts();
// Ensure the strings file wasn't recreated. It's not needed for any other
// tests.
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
EXPECT_FALSE(base::PathExists(strings_file));
EXPECT_EQ(auto_login_util_mock_->GetAddToLoginItemsCalledCount(), 0);
base::FilePath plist_path =
shim_path_.Append("Contents").Append("Info.plist");
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
EXPECT_NSEQ(base::SysUTF8ToNSString(info_->app_id),
plist[app_mode::kCrAppModeShortcutIDKey]);
EXPECT_NSEQ(base::SysUTF16ToNSString(info_->title),
plist[app_mode::kCrAppModeShortcutNameKey]);
EXPECT_NSEQ(base::SysUTF8ToNSString(info_->url.spec()),
plist[app_mode::kCrAppModeShortcutURLKey]);
EXPECT_NSEQ(base::SysUTF8ToNSString(version_info::GetVersionNumber()),
plist[app_mode::kCrBundleVersionKey]);
EXPECT_NSEQ(base::SysUTF8ToNSString(info_->version_for_display),
plist[app_mode::kCFBundleShortVersionStringKey]);
// Make sure all values in the plist are actually filled in.
for (id key in plist) {
id value = [plist valueForKey:key];
if (!base::apple::ObjCCast<NSString>(value)) {
continue;
}
EXPECT_EQ(static_cast<NSUInteger>(NSNotFound),
[value rangeOfString:@"@APP_"].location)
<< base::SysNSStringToUTF8(key) << ":"
<< base::SysNSStringToUTF8(value);
}
}
TEST_F(WebAppShortcutCreatorTest, FileHandlers) {
const base::FilePath plist_path =
shim_path_.Append("Contents").Append("Info.plist");
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
// kCFBundleDocumentTypesKey should not be set, because we set no file
// handlers.
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* doc_types_array = plist[app_mode::kCFBundleDocumentTypesKey];
EXPECT_EQ(doc_types_array, nil);
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
// Register 2 mime types (and 2 invalid extensions). We should now have
// kCFBundleTypeMIMETypesKey but not kCFBundleTypeExtensionsKey.
info_->file_handler_extensions.insert("byobb");
info_->file_handler_extensions.insert(".");
info_->file_handler_mime_types.insert("foo/bar");
info_->file_handler_mime_types.insert("moo/cow");
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* doc_types_array = plist[app_mode::kCFBundleDocumentTypesKey];
EXPECT_NE(doc_types_array, nil);
EXPECT_EQ(1u, [doc_types_array count]);
NSDictionary* doc_types_dict = doc_types_array[0];
EXPECT_NE(doc_types_dict, nil);
NSArray* mime_types = doc_types_dict[app_mode::kCFBundleTypeMIMETypesKey];
EXPECT_NE(mime_types, nil);
NSArray* extensions = doc_types_dict[app_mode::kCFBundleTypeExtensionsKey];
EXPECT_EQ(extensions, nil);
// The mime types should be listed in sorted order (note that sorted order
// does matter for correct behavior).
EXPECT_EQ(2u, [mime_types count]);
EXPECT_NSEQ(mime_types[0], @"foo/bar");
EXPECT_NSEQ(mime_types[1], @"moo/cow");
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
// Register 3 valid extensions (and 2 invalid ones) with the 2 mime types.
info_->file_handler_extensions.insert(".cow");
info_->file_handler_extensions.insert(".pig");
info_->file_handler_extensions.insert(".bbq");
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* doc_types_array = plist[app_mode::kCFBundleDocumentTypesKey];
EXPECT_NE(doc_types_array, nil);
EXPECT_EQ(1u, [doc_types_array count]);
NSDictionary* doc_types_dict = doc_types_array[0];
EXPECT_NE(doc_types_dict, nil);
NSArray* mime_types = doc_types_dict[app_mode::kCFBundleTypeMIMETypesKey];
EXPECT_NE(mime_types, nil);
NSArray* extensions = doc_types_dict[app_mode::kCFBundleTypeExtensionsKey];
EXPECT_NE(extensions, nil);
EXPECT_EQ(2u, [mime_types count]);
EXPECT_NSEQ(mime_types[0], @"foo/bar");
EXPECT_NSEQ(mime_types[1], @"moo/cow");
EXPECT_EQ(3u, [extensions count]);
EXPECT_NSEQ(extensions[0], @"bbq");
EXPECT_NSEQ(extensions[1], @"cow");
EXPECT_NSEQ(extensions[2], @"pig");
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
// Register just extensions.
info_->file_handler_mime_types.clear();
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* doc_types_array = plist[app_mode::kCFBundleDocumentTypesKey];
EXPECT_NE(doc_types_array, nil);
EXPECT_EQ(1u, [doc_types_array count]);
NSDictionary* doc_types_dict = doc_types_array[0];
EXPECT_NE(doc_types_dict, nil);
NSArray* mime_types = doc_types_dict[app_mode::kCFBundleTypeMIMETypesKey];
EXPECT_EQ(mime_types, nil);
NSArray* extensions = doc_types_dict[app_mode::kCFBundleTypeExtensionsKey];
EXPECT_NE(extensions, nil);
EXPECT_EQ(3u, [extensions count]);
EXPECT_NSEQ(extensions[0], @"bbq");
EXPECT_NSEQ(extensions[1], @"cow");
EXPECT_NSEQ(extensions[2], @"pig");
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
// Register extensions and mime types in a separate profile.
const base::FilePath profile1 =
base::FilePath("user_data_dir").Append("Profile 1");
const base::FilePath profile2 =
base::FilePath("user_data_dir").Append("Profile 2");
info_->file_handler_extensions.clear();
info_->handlers_per_profile[profile1].file_handler_extensions = {".cow"};
info_->handlers_per_profile[profile2].file_handler_extensions = {
".bbq", ".pig", ".", "byobb"};
info_->handlers_per_profile[profile1].file_handler_mime_types = {"foo/bar"};
info_->handlers_per_profile[profile2].file_handler_mime_types = {"moo/cow"};
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* doc_types_array = plist[app_mode::kCFBundleDocumentTypesKey];
EXPECT_NE(doc_types_array, nil);
EXPECT_EQ(1u, [doc_types_array count]);
NSDictionary* doc_types_dict = doc_types_array[0];
EXPECT_NE(doc_types_dict, nil);
NSArray* mime_types = doc_types_dict[app_mode::kCFBundleTypeMIMETypesKey];
EXPECT_NE(mime_types, nil);
NSArray* extensions = doc_types_dict[app_mode::kCFBundleTypeExtensionsKey];
EXPECT_NE(extensions, nil);
EXPECT_EQ(2u, [extensions count]);
EXPECT_NSEQ(extensions[0], @"bbq");
EXPECT_NSEQ(extensions[1], @"pig");
EXPECT_EQ(1u, [mime_types count]);
EXPECT_NSEQ(mime_types[0], @"moo/cow");
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
}
TEST_F(WebAppShortcutCreatorTest, ProtocolHandlers) {
const base::FilePath plist_path =
shim_path_.Append("Contents").Append("Info.plist");
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
// CFBundleURLTypes should not be set, because we set no protocol
// handlers.
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* protocol_types_value = plist[app_mode::kCFBundleURLTypesKey];
EXPECT_EQ(protocol_types_value, nil);
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
// Register 2 valid protocol handlers.
info_->protocol_handlers.insert("mailto");
info_->protocol_handlers.insert("web+testing");
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* protocol_types_value = plist[app_mode::kCFBundleURLTypesKey];
EXPECT_NE(protocol_types_value, nil);
EXPECT_EQ(1u, [protocol_types_value count]);
NSDictionary* protocol_types_dict = protocol_types_value[0];
EXPECT_NE(protocol_types_dict, nil);
// Verify CFBundleURLName is set.
EXPECT_NSEQ(protocol_types_dict[app_mode::kCFBundleURLNameKey],
base::SysUTF8ToNSString(base::apple::BaseBundleID() +
std::string(".app.") + info_->app_id));
// Verify CFBundleURLSchemes is set, and contains the expected values.
NSArray* handlers = protocol_types_dict[app_mode::kCFBundleURLSchemesKey];
EXPECT_NE(handlers, nil);
EXPECT_EQ(2u, [handlers count]);
EXPECT_NSEQ(handlers[0], @"mailto");
EXPECT_NSEQ(handlers[1], @"web+testing");
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
// Register protocol handlers in a separate profile.
const base::FilePath profile1 =
base::FilePath("user_data_dir").Append("Profile 1");
const base::FilePath profile2 =
base::FilePath("user_data_dir").Append("Profile 2");
info_->protocol_handlers.clear();
info_->handlers_per_profile[profile1].protocol_handlers = {"mailto"};
info_->handlers_per_profile[profile2].protocol_handlers = {"web+testing"};
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
{
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
NSArray* protocol_types_value = plist[app_mode::kCFBundleURLTypesKey];
EXPECT_NE(protocol_types_value, nil);
EXPECT_EQ(1u, [protocol_types_value count]);
NSDictionary* protocol_types_dict = protocol_types_value[0];
EXPECT_NE(protocol_types_dict, nil);
// Verify CFBundleURLName is set.
EXPECT_NSEQ(protocol_types_dict[app_mode::kCFBundleURLNameKey],
base::SysUTF8ToNSString(base::apple::BaseBundleID() +
std::string(".app.") + info_->app_id));
// Verify CFBundleURLSchemes is set, and contains the expected values.
NSArray* handlers = protocol_types_dict[app_mode::kCFBundleURLSchemesKey];
EXPECT_NE(handlers, nil);
EXPECT_EQ(1u, [handlers count]);
EXPECT_NSEQ(handlers[0], @"web+testing");
}
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
}
TEST_F(WebAppShortcutCreatorTest, CreateShortcutsConflict) {
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
// Create a conflicting .app.
EXPECT_FALSE(base::PathExists(shim_path_));
base::CreateDirectory(shim_path_);
EXPECT_TRUE(base::PathExists(shim_path_));
// Ensure that the " 1.app" path does not yet exist.
base::FilePath conflict_base_name(base::UTF16ToUTF8(info_->title) + " 1.app");
base::FilePath conflict_path = destination_dir_.Append(conflict_base_name);
EXPECT_FALSE(base::PathExists(conflict_path));
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
// We should have created the " 1.app" path.
EXPECT_TRUE(base::PathExists(conflict_path));
EXPECT_TRUE(base::PathExists(destination_dir_));
}
TEST_F(WebAppShortcutCreatorTest, CreateShortcutsStartup) {
WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_.get());
ShortcutLocations locations;
locations.in_startup = true;
auto_login_util_mock_->ResetCounts();
EXPECT_FALSE(base::PathExists(shim_path_));
EXPECT_CALL(shortcut_creator, RevealAppShimInFinder(_)).Times(0);
EXPECT_TRUE(
shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED, locations));
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_EQ(auto_login_util_mock_->GetAddToLoginItemsCalledCount(), 1);
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
}
TEST_F(WebAppShortcutCreatorTest, NormalizeTitle) {
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
info_->title = u"../../Evil/";
EXPECT_EQ(destination_dir_.Append(":..:Evil:.app"),
shortcut_creator.GetApplicationsShortcutPath(false));
info_->title = u"....";
EXPECT_EQ(destination_dir_.Append(fallback_shim_base_name_),
shortcut_creator.GetApplicationsShortcutPath(false));
}
TEST_F(WebAppShortcutCreatorTest, UpdateShortcuts) {
base::ScopedTempDir temp_dir;
EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath update_folder = temp_dir.GetPath();
base::FilePath other_shim_path = update_folder.Append(shim_base_name_);
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
std::vector<base::FilePath> bundle_by_id_paths;
bundle_by_id_paths.push_back(other_shim_path);
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillOnce(Return(bundle_by_id_paths));
EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path));
EXPECT_TRUE(base::DeletePathRecursively(other_shim_path.Append("Contents")));
std::vector<base::FilePath> updated_paths;
EXPECT_TRUE(shortcut_creator.UpdateShortcuts(false, &updated_paths));
EXPECT_FALSE(base::PathExists(shim_path_));
EXPECT_TRUE(base::PathExists(other_shim_path.Append("Contents")));
// The list of updated paths is the paths found by bundle.
EXPECT_EQ(bundle_by_id_paths, updated_paths);
// Also test case where GetAppBundlesByIdUnsorted fails.
bundle_by_id_paths.clear();
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillOnce(Return(bundle_by_id_paths));
EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path));
EXPECT_TRUE(base::DeletePathRecursively(other_shim_path.Append("Contents")));
updated_paths.clear();
EXPECT_TRUE(shortcut_creator.UpdateShortcuts(false, &updated_paths));
EXPECT_TRUE(updated_paths.empty());
EXPECT_FALSE(base::PathExists(shim_path_));
EXPECT_FALSE(base::PathExists(other_shim_path.Append("Contents")));
// Also test case where GetAppBundlesByIdUnsorted fails and recreation is
// forced.
bundle_by_id_paths.clear();
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillOnce(Return(bundle_by_id_paths));
// The default shim path is created along with the internal path.
updated_paths.clear();
EXPECT_TRUE(shortcut_creator.UpdateShortcuts(true, &updated_paths));
EXPECT_EQ(1u, updated_paths.size());
EXPECT_EQ(shim_path_, updated_paths[0]);
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_FALSE(base::PathExists(other_shim_path.Append("Contents")));
}
TEST_F(WebAppShortcutCreatorTest, UpdateShortcutsWithTitleChange) {
base::ScopedTempDir temp_dir;
EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath update_folder = temp_dir.GetPath();
base::FilePath shim_path = update_folder.Append(shim_base_name_);
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
std::vector<base::FilePath> bundle_by_id_paths;
bundle_by_id_paths.push_back(shim_path);
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillOnce(Return(bundle_by_id_paths));
EXPECT_TRUE(shortcut_creator.BuildShortcut(shim_path));
// After building, the bundle should exist and have the right name.
EXPECT_TRUE(base::PathExists(
update_folder.Append("Shortcut Title.app").Append("Contents")));
// The bundle name (in Info.plist) should also be set to 'Shortcut Title'.
base::FilePath plist_path = update_folder.Append("Shortcut Title.app")
.Append("Contents")
.Append("Info.plist");
EXPECT_TRUE(base::PathExists(plist_path));
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
EXPECT_NSEQ(@"Shortcut Title",
plist[base::apple::CFToNSPtrCast(kCFBundleNameKey)]);
// The display name (in InfoPlist.strings) should also be 'Shortcut Title'.
NSString* language = [NSLocale preferredLanguages][0];
std::string locale_dir_name = base::SysNSStringToUTF8(language) + ".lproj";
base::FilePath resource_file_path = plist_path.DirName()
.Append("Resources")
.Append(locale_dir_name)
.Append("InfoPlist.strings");
EXPECT_TRUE(base::PathExists(resource_file_path));
NSDictionary* resources =
[NSDictionary dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(
resource_file_path)
error:nil];
EXPECT_NSEQ(@"Shortcut Title", resources[app_mode::kCFBundleDisplayNameKey]);
// UpdateShortcuts does this as well, but clear the app bundle contents to
// ensure expectations are testing against new data. Keep the top-level path
// to ensure UpdateShortcuts detects its presence.
EXPECT_TRUE(base::DeletePathRecursively(shim_path.Append("Contents")));
std::vector<base::FilePath> updated_paths;
EXPECT_TRUE(shortcut_creator.UpdateShortcuts(false, &updated_paths));
EXPECT_EQ(
std::vector<base::FilePath>{update_folder.Append("Shortcut Title.app")},
updated_paths);
// After updating, the bundle should still exist and have the same name.
EXPECT_TRUE(base::PathExists(
update_folder.Append("Shortcut Title.app").Append("Contents")));
// The bundle name (in Info.plist) should still be set to 'Shortcut Title'.
plist_path = update_folder.Append("Shortcut Title.app")
.Append("Contents")
.Append("Info.plist");
EXPECT_TRUE(base::PathExists(plist_path));
plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
EXPECT_NSEQ(@"Shortcut Title",
plist[base::apple::CFToNSPtrCast(kCFBundleNameKey)]);
// The display name (in InfoPlist.strings) should still be 'Shortcut Title'.
resource_file_path = plist_path.DirName()
.Append("Resources")
.Append(locale_dir_name)
.Append("InfoPlist.strings");
EXPECT_TRUE(base::PathExists(resource_file_path));
resources =
[NSDictionary dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(
resource_file_path)
error:nil];
EXPECT_NSEQ(@"Shortcut Title", resources[app_mode::kCFBundleDisplayNameKey]);
// Now simulate an update with a different title.
std::unique_ptr<ShortcutInfo> info = GetShortcutInfo();
info->title = u"New App Title";
NiceMock<WebAppShortcutCreatorMock> shortcut_creator2(app_data_dir_,
info.get());
EXPECT_CALL(shortcut_creator2, GetAppBundlesByIdUnsorted())
.WillOnce(Return(bundle_by_id_paths));
updated_paths.clear();
EXPECT_TRUE(shortcut_creator2.UpdateShortcuts(false, &updated_paths));
EXPECT_EQ(
std::vector<base::FilePath>{update_folder.Append("Shortcut Title.app")},
updated_paths);
// After updating, the bundle should not have changed its name. Note that this
// assumes an entirely new bundle wasn't created, which is verified by
// expectations below.
EXPECT_TRUE(base::PathExists(
update_folder.Append("Shortcut Title.app").Append("Contents")));
// The bundle name (in Info.plist) should also still be set to 'Shortcut
// Title'.
plist_path = update_folder.Append("Shortcut Title.app")
.Append("Contents")
.Append("Info.plist");
EXPECT_TRUE(base::PathExists(plist_path));
plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
EXPECT_NSEQ(@"Shortcut Title",
plist[base::apple::CFToNSPtrCast(kCFBundleNameKey)]);
// The display name (in InfoPlist.strings) should have changed to 'New App
// Title'.
resource_file_path = plist_path.DirName()
.Append("Resources")
.Append(locale_dir_name)
.Append("InfoPlist.strings");
EXPECT_TRUE(base::PathExists(resource_file_path));
resources =
[NSDictionary dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(
resource_file_path)
error:nil];
EXPECT_NSEQ(@"New App Title", resources[app_mode::kCFBundleDisplayNameKey]);
}
TEST_F(WebAppShortcutCreatorTest, UpdateBookmarkAppShortcut) {
base::ScopedTempDir other_folder_temp_dir;
EXPECT_TRUE(other_folder_temp_dir.CreateUniqueTempDir());
base::FilePath other_folder = other_folder_temp_dir.GetPath();
base::FilePath other_shim_path = other_folder.Append(shim_base_name_);
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
std::vector<base::FilePath> bundle_by_id_paths;
bundle_by_id_paths.push_back(shim_path_);
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillOnce(Return(bundle_by_id_paths));
EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path));
EXPECT_TRUE(base::DeletePathRecursively(other_shim_path));
// The original shim should be recreated.
std::vector<base::FilePath> updated_paths;
EXPECT_TRUE(shortcut_creator.UpdateShortcuts(false, &updated_paths));
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_FALSE(base::PathExists(other_shim_path.Append("Contents")));
}
TEST_F(WebAppShortcutCreatorTest, NormalizeColonsInDisplayName) {
base::ScopedTempDir temp_dir;
EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath update_folder = temp_dir.GetPath();
base::FilePath shim_path = update_folder.Append("App Title: New.app");
std::unique_ptr<ShortcutInfo> info = GetShortcutInfo();
info->title = u"App Title: New";
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info.get());
std::vector<base::FilePath> bundle_by_id_paths;
bundle_by_id_paths.push_back(shim_path);
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillOnce(Return(bundle_by_id_paths));
EXPECT_TRUE(shortcut_creator.BuildShortcut(shim_path));
std::vector<base::FilePath> updated_paths;
EXPECT_TRUE(shortcut_creator.UpdateShortcuts(false, &updated_paths));
// After building, the bundle should exist and have the right name.
EXPECT_TRUE(base::PathExists(
update_folder.Append("App Title: New.app").Append("Contents")));
// The bundle name (in Info.plist) should also match.
base::FilePath plist_path = update_folder.Append("App Title: New.app")
.Append("Contents")
.Append("Info.plist");
EXPECT_TRUE(base::PathExists(plist_path));
NSDictionary* plist = [NSDictionary
dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(plist_path)
error:nil];
EXPECT_NSEQ(@"App Title: New",
plist[base::apple::CFToNSPtrCast(kCFBundleNameKey)]);
// The display name (in InfoPlist.strings) should not have the colon.
NSString* language = [NSLocale preferredLanguages][0];
std::string locale_dir_name = base::SysNSStringToUTF8(language) + ".lproj";
base::FilePath resource_file_path = plist_path.DirName()
.Append("Resources")
.Append(locale_dir_name)
.Append("InfoPlist.strings");
EXPECT_TRUE(base::PathExists(resource_file_path));
NSDictionary* resources =
[NSDictionary dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(
resource_file_path)
error:nil];
EXPECT_NSEQ(@"App Title New", resources[app_mode::kCFBundleDisplayNameKey]);
}
TEST_F(WebAppShortcutCreatorTest, DeleteShortcutsSingleProfile) {
info_->is_multi_profile = false;
base::FilePath other_shim_path =
shim_path_.DirName().Append("Copy of Shim.app");
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
// Create an extra shim in another folder. It should be deleted since its
// bundle id matches.
std::vector<base::FilePath> bundle_by_id_paths;
bundle_by_id_paths.push_back(shim_path_);
bundle_by_id_paths.push_back(other_shim_path);
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillRepeatedly(Return(bundle_by_id_paths));
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
// Ensure the paths were created, and that they are destroyed.
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_TRUE(base::PathExists(other_shim_path));
auto_login_util_mock_->ResetCounts();
internals::DeleteMultiProfileShortcutsForApp(info_->app_id);
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 0);
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_TRUE(base::PathExists(other_shim_path));
auto_login_util_mock_->ResetCounts();
internals::DeletePlatformShortcuts(
app_data_dir_, *info_, task_environment_.GetMainThreadTaskRunner(),
base::DoNothing());
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 2);
EXPECT_FALSE(base::PathExists(shim_path_));
EXPECT_FALSE(base::PathExists(other_shim_path));
}
TEST_F(WebAppShortcutCreatorTest, DeleteShortcuts) {
base::FilePath other_shim_path =
shim_path_.DirName().Append("Copy of Shim.app");
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
// Create an extra shim in another folder. It should be deleted since its
// bundle id matches.
std::vector<base::FilePath> bundle_by_id_paths;
bundle_by_id_paths.push_back(shim_path_);
bundle_by_id_paths.push_back(other_shim_path);
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillRepeatedly(Return(bundle_by_id_paths));
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
// Ensure the paths were created, and that they are destroyed.
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_TRUE(base::PathExists(other_shim_path));
auto_login_util_mock_->ResetCounts();
internals::DeletePlatformShortcuts(
app_data_dir_, *info_, task_environment_.GetMainThreadTaskRunner(),
base::DoNothing());
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 0);
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_TRUE(base::PathExists(other_shim_path));
auto_login_util_mock_->ResetCounts();
internals::DeleteMultiProfileShortcutsForApp(info_->app_id);
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 2);
EXPECT_FALSE(base::PathExists(shim_path_));
EXPECT_FALSE(base::PathExists(other_shim_path));
}
TEST_F(WebAppShortcutCreatorTest, DeleteAllShortcutsForProfile) {
info_->is_multi_profile = false;
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
base::FilePath profile_path = info_->profile_path;
base::FilePath other_profile_path =
profile_path.DirName().Append("Profile 2");
EXPECT_FALSE(base::PathExists(shim_path_));
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
EXPECT_TRUE(base::PathExists(shim_path_));
auto_login_util_mock_->ResetCounts();
internals::DeleteAllShortcutsForProfile(other_profile_path);
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 0);
EXPECT_TRUE(base::PathExists(shim_path_));
auto_login_util_mock_->ResetCounts();
internals::DeleteAllShortcutsForProfile(profile_path);
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 1);
EXPECT_FALSE(base::PathExists(shim_path_));
}
TEST_F(WebAppShortcutCreatorTest, RunShortcut) {
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
EXPECT_TRUE(base::PathExists(shim_path_));
ssize_t status =
getxattr(shim_path_.value().c_str(), "com.apple.quarantine",
/*value=*/nullptr, /*size=*/0, /*position=*/0, /*options=*/0);
EXPECT_EQ(-1, status);
EXPECT_EQ(ENOATTR, errno);
}
TEST_F(WebAppShortcutCreatorTest, CreateFailure) {
ASSERT_TRUE(override_registration_->test_override().DeleteChromeAppsDir());
NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
info_.get());
EXPECT_FALSE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
}
TEST_F(WebAppShortcutCreatorTest, UpdateIcon) {
gfx::Image product_logo_16 =
ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
IDR_PRODUCT_LOGO_16);
gfx::Image product_logo_32 =
ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
IDR_PRODUCT_LOGO_32);
WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_.get());
base::FilePath icon_path =
shim_path_.Append("Contents").Append("Resources").Append("app.icns");
// regular favicon should be used if no maskable favicons exist
info_->favicon.Add(product_logo_32);
ASSERT_TRUE(shortcut_creator.UpdateIcon(shim_path_));
NSImage* image = [[NSImage alloc]
initWithContentsOfFile:base::apple::FilePathToNSString(icon_path)];
EXPECT_TRUE(image);
EXPECT_EQ(product_logo_32.Width(), image.size.width);
EXPECT_EQ(product_logo_32.Height(), image.size.height);
// maskable favicon should be used if present
info_->favicon_maskable.Add(product_logo_16);
ASSERT_TRUE(shortcut_creator.UpdateIcon(shim_path_));
image = [[NSImage alloc]
initWithContentsOfFile:base::apple::FilePathToNSString(icon_path)];
EXPECT_TRUE(image);
EXPECT_EQ(product_logo_16.Width(), image.size.width);
EXPECT_EQ(product_logo_16.Height(), image.size.height);
}
TEST_F(WebAppShortcutCreatorTest, RevealAppShimInFinder) {
WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_.get());
EXPECT_CALL(shortcut_creator, RevealAppShimInFinder(_)).Times(0);
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED,
ShortcutLocations()));
EXPECT_CALL(shortcut_creator, RevealAppShimInFinder(_));
EXPECT_TRUE(shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_BY_USER,
ShortcutLocations()));
}
TEST_F(WebAppShortcutCreatorTest, SortAppBundles) {
base::FilePath app_dir("/home/apps");
NiceMock<WebAppShortcutCreatorSortingMock> shortcut_creator(app_dir,
info_.get());
base::FilePath a = shortcut_creator.GetApplicationsShortcutPath(false);
base::FilePath b = GetChromeAppsFolder().Append("a");
base::FilePath c = GetChromeAppsFolder().Append("z");
base::FilePath d("/a/b/c");
base::FilePath e("/z/y/w");
std::vector<base::FilePath> unsorted = {e, c, a, d, b};
std::vector<base::FilePath> sorted = {a, b, c, d, e};
EXPECT_CALL(shortcut_creator, GetAppBundlesByIdUnsorted())
.WillOnce(Return(unsorted));
std::vector<base::FilePath> result = shortcut_creator.GetAppBundlesById();
EXPECT_EQ(result, sorted);
}
TEST_F(WebAppShortcutCreatorTest, RemoveAppShimFromLoginItems) {
WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_.get());
ShortcutLocations locations;
locations.in_startup = true;
auto_login_util_mock_->ResetCounts();
EXPECT_FALSE(base::PathExists(shim_path_));
EXPECT_CALL(shortcut_creator, RevealAppShimInFinder(_)).Times(0);
EXPECT_TRUE(
shortcut_creator.CreateShortcuts(SHORTCUT_CREATION_AUTOMATED, locations));
EXPECT_TRUE(base::PathExists(shim_path_));
EXPECT_EQ(auto_login_util_mock_->GetAddToLoginItemsCalledCount(), 1);
auto_login_util_mock_->ResetCounts();
RemoveAppShimFromLoginItems("does-not-exist-app");
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 0);
auto_login_util_mock_->ResetCounts();
RemoveAppShimFromLoginItems(info_->app_id);
EXPECT_EQ(auto_login_util_mock_->GetRemoveFromLoginItemsCalledCount(), 1);
EXPECT_TRUE(base::DeletePathRecursively(shim_path_));
}
} // namespace web_app