1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/web_applications/web_app_mac.h"
7 #import <Cocoa/Cocoa.h>
11 #include "base/command_line.h"
12 #include "base/files/file_util.h"
13 #include "base/files/scoped_temp_dir.h"
14 #include "base/mac/foundation_util.h"
15 #include "base/mac/scoped_nsobject.h"
16 #include "base/memory/scoped_ptr.h"
17 #include "base/path_service.h"
18 #include "base/strings/sys_string_conversions.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "chrome/common/chrome_paths.h"
21 #include "chrome/common/chrome_switches.h"
22 #include "chrome/common/chrome_version_info.h"
23 #import "chrome/common/mac/app_mode_common.h"
24 #include "grit/theme_resources.h"
25 #include "testing/gmock/include/gmock/gmock.h"
26 #include "testing/gtest/include/gtest/gtest.h"
27 #import "testing/gtest_mac.h"
28 #include "third_party/skia/include/core/SkBitmap.h"
29 #include "ui/base/resource/resource_bundle.h"
30 #include "ui/gfx/image/image.h"
33 using ::testing::Return;
34 using ::testing::NiceMock;
38 const char kFakeChromeBundleId[] = "fake.cfbundleidentifier";
40 class WebAppShortcutCreatorMock : public web_app::WebAppShortcutCreator {
42 WebAppShortcutCreatorMock(const base::FilePath& app_data_dir,
43 const web_app::ShortcutInfo* shortcut_info)
44 : WebAppShortcutCreator(app_data_dir,
46 extensions::FileHandlersInfo()) {}
48 WebAppShortcutCreatorMock(
49 const base::FilePath& app_data_dir,
50 const web_app::ShortcutInfo* shortcut_info,
51 const extensions::FileHandlersInfo& file_handlers_info)
52 : WebAppShortcutCreator(app_data_dir, shortcut_info, file_handlers_info) {
55 MOCK_CONST_METHOD0(GetApplicationsDirname, base::FilePath());
56 MOCK_CONST_METHOD1(GetAppBundleById,
57 base::FilePath(const std::string& bundle_id));
58 MOCK_CONST_METHOD0(RevealAppShimInFinder, void());
61 DISALLOW_COPY_AND_ASSIGN(WebAppShortcutCreatorMock);
64 scoped_ptr<web_app::ShortcutInfo> GetShortcutInfo() {
65 scoped_ptr<web_app::ShortcutInfo> info(new web_app::ShortcutInfo);
66 info->extension_id = "extensionid";
67 info->extension_path = base::FilePath("/fake/extension/path");
68 info->title = base::ASCIIToUTF16("Shortcut Title");
69 info->url = GURL("http://example.com/");
70 info->profile_path = base::FilePath("user_data_dir").Append("Profile 1");
71 info->profile_name = "profile name";
72 info->version_for_display = "stable 1.0";
76 class WebAppShortcutCreatorTest : public testing::Test {
78 WebAppShortcutCreatorTest() {}
80 void SetUp() override {
81 base::mac::SetBaseBundleID(kFakeChromeBundleId);
83 EXPECT_TRUE(temp_app_data_dir_.CreateUniqueTempDir());
84 EXPECT_TRUE(temp_destination_dir_.CreateUniqueTempDir());
85 app_data_dir_ = temp_app_data_dir_.path();
86 destination_dir_ = temp_destination_dir_.path();
88 info_ = GetShortcutInfo();
89 shim_base_name_ = base::FilePath(info_->profile_path.BaseName().value() +
90 " " + info_->extension_id + ".app");
91 internal_shim_path_ = app_data_dir_.Append(shim_base_name_);
92 shim_path_ = destination_dir_.Append(shim_base_name_);
95 base::ScopedTempDir temp_app_data_dir_;
96 base::ScopedTempDir temp_destination_dir_;
97 base::FilePath app_data_dir_;
98 base::FilePath destination_dir_;
100 scoped_ptr<web_app::ShortcutInfo> info_;
101 base::FilePath shim_base_name_;
102 base::FilePath internal_shim_path_;
103 base::FilePath shim_path_;
106 DISALLOW_COPY_AND_ASSIGN(WebAppShortcutCreatorTest);
114 TEST_F(WebAppShortcutCreatorTest, CreateShortcuts) {
115 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
117 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
118 .WillRepeatedly(Return(destination_dir_));
120 EXPECT_TRUE(shortcut_creator.CreateShortcuts(
121 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations()));
122 EXPECT_TRUE(base::PathExists(shim_path_));
123 EXPECT_TRUE(base::PathExists(destination_dir_));
124 EXPECT_EQ(shim_base_name_, shortcut_creator.GetShortcutBasename());
126 base::FilePath plist_path =
127 shim_path_.Append("Contents").Append("Info.plist");
128 NSDictionary* plist = [NSDictionary dictionaryWithContentsOfFile:
129 base::mac::FilePathToNSString(plist_path)];
130 EXPECT_NSEQ(base::SysUTF8ToNSString(info_->extension_id),
131 [plist objectForKey:app_mode::kCrAppModeShortcutIDKey]);
132 EXPECT_NSEQ(base::SysUTF16ToNSString(info_->title),
133 [plist objectForKey:app_mode::kCrAppModeShortcutNameKey]);
134 EXPECT_NSEQ(base::SysUTF8ToNSString(info_->url.spec()),
135 [plist objectForKey:app_mode::kCrAppModeShortcutURLKey]);
137 EXPECT_NSEQ(base::SysUTF8ToNSString(chrome::VersionInfo().Version()),
138 [plist objectForKey:app_mode::kCrBundleVersionKey]);
139 EXPECT_NSEQ(base::SysUTF8ToNSString(info_->version_for_display),
140 [plist objectForKey:app_mode::kCFBundleShortVersionStringKey]);
142 // Make sure all values in the plist are actually filled in.
143 for (id key in plist) {
144 id value = [plist valueForKey:key];
145 if (!base::mac::ObjCCast<NSString>(value))
148 EXPECT_EQ([value rangeOfString:@"@APP_"].location, NSNotFound)
149 << [key UTF8String] << ":" << [value UTF8String];
153 TEST_F(WebAppShortcutCreatorTest, UpdateShortcuts) {
154 base::ScopedTempDir other_folder_temp_dir;
155 EXPECT_TRUE(other_folder_temp_dir.CreateUniqueTempDir());
156 base::FilePath other_folder = other_folder_temp_dir.path();
157 base::FilePath other_shim_path = other_folder.Append(shim_base_name_);
159 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
161 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
162 .WillRepeatedly(Return(destination_dir_));
164 std::string expected_bundle_id = kFakeChromeBundleId;
165 expected_bundle_id += ".app.Profile-1-" + info_->extension_id;
166 EXPECT_CALL(shortcut_creator, GetAppBundleById(expected_bundle_id))
167 .WillOnce(Return(other_shim_path));
169 EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path));
171 EXPECT_TRUE(base::DeleteFile(other_shim_path.Append("Contents"), true));
173 EXPECT_TRUE(shortcut_creator.UpdateShortcuts());
174 EXPECT_FALSE(base::PathExists(shim_path_));
175 EXPECT_TRUE(base::PathExists(other_shim_path.Append("Contents")));
177 // Also test case where GetAppBundleById fails.
178 EXPECT_CALL(shortcut_creator, GetAppBundleById(expected_bundle_id))
179 .WillOnce(Return(base::FilePath()));
181 EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path));
183 EXPECT_TRUE(base::DeleteFile(other_shim_path.Append("Contents"), true));
185 EXPECT_FALSE(shortcut_creator.UpdateShortcuts());
186 EXPECT_FALSE(base::PathExists(shim_path_));
187 EXPECT_FALSE(base::PathExists(other_shim_path.Append("Contents")));
190 TEST_F(WebAppShortcutCreatorTest, DeleteShortcuts) {
191 // When using PathService::Override, it calls base::MakeAbsoluteFilePath.
192 // On Mac this prepends "/private" to the path, but points to the same
193 // directory in the file system.
194 app_data_dir_ = base::MakeAbsoluteFilePath(app_data_dir_);
196 base::ScopedTempDir other_folder_temp_dir;
197 EXPECT_TRUE(other_folder_temp_dir.CreateUniqueTempDir());
198 base::FilePath other_folder = other_folder_temp_dir.path();
199 base::FilePath other_shim_path = other_folder.Append(shim_base_name_);
201 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
203 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
204 .WillRepeatedly(Return(destination_dir_));
206 std::string expected_bundle_id = kFakeChromeBundleId;
207 expected_bundle_id += ".app.Profile-1-" + info_->extension_id;
208 EXPECT_CALL(shortcut_creator, GetAppBundleById(expected_bundle_id))
209 .WillOnce(Return(other_shim_path));
211 EXPECT_TRUE(shortcut_creator.CreateShortcuts(
212 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations()));
213 EXPECT_TRUE(base::PathExists(internal_shim_path_));
214 EXPECT_TRUE(base::PathExists(shim_path_));
216 // Create an extra shim in another folder. It should be deleted since its
217 // bundle id matches.
218 EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path));
219 EXPECT_TRUE(base::PathExists(other_shim_path));
221 // Change the user_data_dir of the shim at shim_path_. It should not be
222 // deleted since its user_data_dir does not match.
223 NSString* plist_path = base::mac::FilePathToNSString(
224 shim_path_.Append("Contents").Append("Info.plist"));
225 NSMutableDictionary* plist =
226 [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
227 [plist setObject:@"fake_user_data_dir"
228 forKey:app_mode::kCrAppModeUserDataDirKey];
229 [plist writeToFile:plist_path
232 EXPECT_TRUE(PathService::Override(chrome::DIR_USER_DATA, app_data_dir_));
233 shortcut_creator.DeleteShortcuts();
234 EXPECT_FALSE(base::PathExists(internal_shim_path_));
235 EXPECT_TRUE(base::PathExists(shim_path_));
236 EXPECT_FALSE(base::PathExists(other_shim_path));
239 TEST_F(WebAppShortcutCreatorTest, CreateAppListShortcut) {
240 // With an empty |profile_name|, the shortcut path should not have the profile
241 // directory prepended to the extension id on the app bundle name.
242 info_->profile_name.clear();
243 base::FilePath dst_path =
244 destination_dir_.Append(info_->extension_id + ".app");
246 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(base::FilePath(),
248 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
249 .WillRepeatedly(Return(destination_dir_));
250 EXPECT_EQ(dst_path.BaseName(), shortcut_creator.GetShortcutBasename());
253 TEST_F(WebAppShortcutCreatorTest, RunShortcut) {
254 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
256 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
257 .WillRepeatedly(Return(destination_dir_));
259 EXPECT_TRUE(shortcut_creator.CreateShortcuts(
260 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations()));
261 EXPECT_TRUE(base::PathExists(shim_path_));
263 ssize_t status = getxattr(
264 shim_path_.value().c_str(), "com.apple.quarantine", NULL, 0, 0, 0);
265 EXPECT_EQ(-1, status);
266 EXPECT_EQ(ENOATTR, errno);
269 TEST_F(WebAppShortcutCreatorTest, CreateFailure) {
270 base::FilePath non_existent_path =
271 destination_dir_.Append("not-existent").Append("name.app");
273 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_,
275 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
276 .WillRepeatedly(Return(non_existent_path));
277 EXPECT_FALSE(shortcut_creator.CreateShortcuts(
278 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations()));
281 TEST_F(WebAppShortcutCreatorTest, UpdateIcon) {
282 gfx::Image product_logo =
283 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
284 IDR_PRODUCT_LOGO_32);
285 info_->favicon.Add(product_logo);
286 WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_.get());
288 ASSERT_TRUE(shortcut_creator.UpdateIcon(shim_path_));
289 base::FilePath icon_path =
290 shim_path_.Append("Contents").Append("Resources").Append("app.icns");
292 base::scoped_nsobject<NSImage> image([[NSImage alloc]
293 initWithContentsOfFile:base::mac::FilePathToNSString(icon_path)]);
295 EXPECT_EQ(product_logo.Width(), [image size].width);
296 EXPECT_EQ(product_logo.Height(), [image size].height);
299 TEST_F(WebAppShortcutCreatorTest, RevealAppShimInFinder) {
300 WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_.get());
301 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
302 .WillRepeatedly(Return(destination_dir_));
304 EXPECT_CALL(shortcut_creator, RevealAppShimInFinder())
306 EXPECT_TRUE(shortcut_creator.CreateShortcuts(
307 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations()));
309 EXPECT_CALL(shortcut_creator, RevealAppShimInFinder());
310 EXPECT_TRUE(shortcut_creator.CreateShortcuts(
311 SHORTCUT_CREATION_BY_USER, web_app::ShortcutLocations()));
314 TEST_F(WebAppShortcutCreatorTest, FileHandlers) {
315 base::CommandLine::ForCurrentProcess()->AppendSwitch(
316 switches::kEnableAppsFileAssociations);
317 extensions::FileHandlersInfo file_handlers_info;
318 extensions::FileHandlerInfo handler_0;
319 handler_0.extensions.insert("ext0");
320 handler_0.extensions.insert("ext1");
321 handler_0.types.insert("type0");
322 handler_0.types.insert("type1");
323 file_handlers_info.push_back(handler_0);
324 extensions::FileHandlerInfo handler_1;
325 handler_1.extensions.insert("ext2");
326 handler_1.types.insert("type2");
327 file_handlers_info.push_back(handler_1);
329 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(
330 app_data_dir_, info_.get(), file_handlers_info);
331 EXPECT_CALL(shortcut_creator, GetApplicationsDirname())
332 .WillRepeatedly(Return(destination_dir_));
333 EXPECT_TRUE(shortcut_creator.CreateShortcuts(
334 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations()));
336 base::FilePath plist_path =
337 shim_path_.Append("Contents").Append("Info.plist");
338 NSDictionary* plist = [NSDictionary
339 dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)];
340 NSArray* file_handlers =
341 [plist objectForKey:app_mode::kCFBundleDocumentTypesKey];
343 NSDictionary* file_handler_0 = [file_handlers objectAtIndex:0];
344 EXPECT_NSEQ(app_mode::kBundleTypeRoleViewer,
345 [file_handler_0 objectForKey:app_mode::kCFBundleTypeRoleKey]);
346 NSArray* file_handler_0_extensions =
347 [file_handler_0 objectForKey:app_mode::kCFBundleTypeExtensionsKey];
348 EXPECT_TRUE([file_handler_0_extensions containsObject:@"ext0"]);
349 EXPECT_TRUE([file_handler_0_extensions containsObject:@"ext1"]);
350 NSArray* file_handler_0_types =
351 [file_handler_0 objectForKey:app_mode::kCFBundleTypeMIMETypesKey];
352 EXPECT_TRUE([file_handler_0_types containsObject:@"type0"]);
353 EXPECT_TRUE([file_handler_0_types containsObject:@"type1"]);
355 NSDictionary* file_handler_1 = [file_handlers objectAtIndex:1];
356 EXPECT_NSEQ(app_mode::kBundleTypeRoleViewer,
357 [file_handler_1 objectForKey:app_mode::kCFBundleTypeRoleKey]);
358 NSArray* file_handler_1_extensions =
359 [file_handler_1 objectForKey:app_mode::kCFBundleTypeExtensionsKey];
360 EXPECT_TRUE([file_handler_1_extensions containsObject:@"ext2"]);
361 NSArray* file_handler_1_types =
362 [file_handler_1 objectForKey:app_mode::kCFBundleTypeMIMETypesKey];
363 EXPECT_TRUE([file_handler_1_types containsObject:@"type2"]);
366 } // namespace web_app