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. 4 5 #import "chrome/browser/web_applications/web_app_mac.h" 6 7 #import <Cocoa/Cocoa.h> 8 #include <errno.h> 9 #include <sys/xattr.h> 10 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/path_service.h" 17 #include "base/strings/sys_string_conversions.h" 18 #include "base/strings/utf_string_conversions.h" 19 #include "chrome/common/chrome_paths.h" 20 #include "chrome/common/chrome_switches.h" 21 #import "chrome/common/mac/app_mode_common.h" 22 #include "grit/theme_resources.h" 23 #include "testing/gmock/include/gmock/gmock.h" 24 #include "testing/gtest/include/gtest/gtest.h" 25 #import "testing/gtest_mac.h" 26 #include "third_party/skia/include/core/SkBitmap.h" 27 #include "ui/base/resource/resource_bundle.h" 28 #include "ui/gfx/image/image.h" 29 30 using ::testing::_; 31 using ::testing::Return; 32 using ::testing::NiceMock; 33 34 namespace { 35 36 const char kFakeChromeBundleId[] = "fake.cfbundleidentifier"; 37 38 class WebAppShortcutCreatorMock : public web_app::WebAppShortcutCreator { 39 public: 40 WebAppShortcutCreatorMock(const base::FilePath& app_data_dir, 41 const web_app::ShortcutInfo& shortcut_info) 42 : WebAppShortcutCreator(app_data_dir, 43 shortcut_info, 44 extensions::FileHandlersInfo()) {} 45 46 WebAppShortcutCreatorMock( 47 const base::FilePath& app_data_dir, 48 const web_app::ShortcutInfo& shortcut_info, 49 const extensions::FileHandlersInfo& file_handlers_info) 50 : WebAppShortcutCreator(app_data_dir, shortcut_info, file_handlers_info) { 51 } 52 53 MOCK_CONST_METHOD0(GetApplicationsDirname, base::FilePath()); 54 MOCK_CONST_METHOD1(GetAppBundleById, 55 base::FilePath(const std::string& bundle_id)); 56 MOCK_CONST_METHOD0(RevealAppShimInFinder, void()); 57 58 private: 59 DISALLOW_COPY_AND_ASSIGN(WebAppShortcutCreatorMock); 60 }; 61 62 web_app::ShortcutInfo GetShortcutInfo() { 63 web_app::ShortcutInfo info; 64 info.extension_id = "extensionid"; 65 info.extension_path = base::FilePath("/fake/extension/path"); 66 info.title = base::ASCIIToUTF16("Shortcut Title"); 67 info.url = GURL("http://example.com/"); 68 info.profile_path = base::FilePath("user_data_dir").Append("Profile 1"); 69 info.profile_name = "profile name"; 70 return info; 71 } 72 73 class WebAppShortcutCreatorTest : public testing::Test { 74 protected: 75 WebAppShortcutCreatorTest() {} 76 77 virtual void SetUp() { 78 base::mac::SetBaseBundleID(kFakeChromeBundleId); 79 80 EXPECT_TRUE(temp_app_data_dir_.CreateUniqueTempDir()); 81 EXPECT_TRUE(temp_destination_dir_.CreateUniqueTempDir()); 82 app_data_dir_ = temp_app_data_dir_.path(); 83 destination_dir_ = temp_destination_dir_.path(); 84 85 info_ = GetShortcutInfo(); 86 shim_base_name_ = base::FilePath( 87 info_.profile_path.BaseName().value() + 88 " " + info_.extension_id + ".app"); 89 internal_shim_path_ = app_data_dir_.Append(shim_base_name_); 90 shim_path_ = destination_dir_.Append(shim_base_name_); 91 } 92 93 base::ScopedTempDir temp_app_data_dir_; 94 base::ScopedTempDir temp_destination_dir_; 95 base::FilePath app_data_dir_; 96 base::FilePath destination_dir_; 97 98 web_app::ShortcutInfo info_; 99 base::FilePath shim_base_name_; 100 base::FilePath internal_shim_path_; 101 base::FilePath shim_path_; 102 103 private: 104 DISALLOW_COPY_AND_ASSIGN(WebAppShortcutCreatorTest); 105 }; 106 107 108 } // namespace 109 110 namespace web_app { 111 112 TEST_F(WebAppShortcutCreatorTest, CreateShortcuts) { 113 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_, info_); 114 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 115 .WillRepeatedly(Return(destination_dir_)); 116 117 EXPECT_TRUE(shortcut_creator.CreateShortcuts( 118 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations())); 119 EXPECT_TRUE(base::PathExists(shim_path_)); 120 EXPECT_TRUE(base::PathExists(destination_dir_)); 121 EXPECT_EQ(shim_base_name_, shortcut_creator.GetShortcutBasename()); 122 123 base::FilePath plist_path = 124 shim_path_.Append("Contents").Append("Info.plist"); 125 NSDictionary* plist = [NSDictionary dictionaryWithContentsOfFile: 126 base::mac::FilePathToNSString(plist_path)]; 127 EXPECT_NSEQ(base::SysUTF8ToNSString(info_.extension_id), 128 [plist objectForKey:app_mode::kCrAppModeShortcutIDKey]); 129 EXPECT_NSEQ(base::SysUTF16ToNSString(info_.title), 130 [plist objectForKey:app_mode::kCrAppModeShortcutNameKey]); 131 EXPECT_NSEQ(base::SysUTF8ToNSString(info_.url.spec()), 132 [plist objectForKey:app_mode::kCrAppModeShortcutURLKey]); 133 134 // Make sure all values in the plist are actually filled in. 135 for (id key in plist) { 136 id value = [plist valueForKey:key]; 137 if (!base::mac::ObjCCast<NSString>(value)) 138 continue; 139 140 EXPECT_EQ([value rangeOfString:@"@APP_"].location, NSNotFound) 141 << [key UTF8String] << ":" << [value UTF8String]; 142 } 143 } 144 145 TEST_F(WebAppShortcutCreatorTest, UpdateShortcuts) { 146 base::ScopedTempDir other_folder_temp_dir; 147 EXPECT_TRUE(other_folder_temp_dir.CreateUniqueTempDir()); 148 base::FilePath other_folder = other_folder_temp_dir.path(); 149 base::FilePath other_shim_path = other_folder.Append(shim_base_name_); 150 151 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_, info_); 152 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 153 .WillRepeatedly(Return(destination_dir_)); 154 155 std::string expected_bundle_id = kFakeChromeBundleId; 156 expected_bundle_id += ".app.Profile-1-" + info_.extension_id; 157 EXPECT_CALL(shortcut_creator, GetAppBundleById(expected_bundle_id)) 158 .WillOnce(Return(other_shim_path)); 159 160 EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path)); 161 162 EXPECT_TRUE(base::DeleteFile(other_shim_path.Append("Contents"), true)); 163 164 EXPECT_TRUE(shortcut_creator.UpdateShortcuts()); 165 EXPECT_FALSE(base::PathExists(shim_path_)); 166 EXPECT_TRUE(base::PathExists(other_shim_path.Append("Contents"))); 167 168 // Also test case where GetAppBundleById fails. 169 EXPECT_CALL(shortcut_creator, GetAppBundleById(expected_bundle_id)) 170 .WillOnce(Return(base::FilePath())); 171 172 EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path)); 173 174 EXPECT_TRUE(base::DeleteFile(other_shim_path.Append("Contents"), true)); 175 176 EXPECT_FALSE(shortcut_creator.UpdateShortcuts()); 177 EXPECT_FALSE(base::PathExists(shim_path_)); 178 EXPECT_FALSE(base::PathExists(other_shim_path.Append("Contents"))); 179 } 180 181 TEST_F(WebAppShortcutCreatorTest, DeleteShortcuts) { 182 // When using PathService::Override, it calls base::MakeAbsoluteFilePath. 183 // On Mac this prepends "/private" to the path, but points to the same 184 // directory in the file system. 185 app_data_dir_ = base::MakeAbsoluteFilePath(app_data_dir_); 186 187 base::ScopedTempDir other_folder_temp_dir; 188 EXPECT_TRUE(other_folder_temp_dir.CreateUniqueTempDir()); 189 base::FilePath other_folder = other_folder_temp_dir.path(); 190 base::FilePath other_shim_path = other_folder.Append(shim_base_name_); 191 192 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_, info_); 193 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 194 .WillRepeatedly(Return(destination_dir_)); 195 196 std::string expected_bundle_id = kFakeChromeBundleId; 197 expected_bundle_id += ".app.Profile-1-" + info_.extension_id; 198 EXPECT_CALL(shortcut_creator, GetAppBundleById(expected_bundle_id)) 199 .WillOnce(Return(other_shim_path)); 200 201 EXPECT_TRUE(shortcut_creator.CreateShortcuts( 202 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations())); 203 EXPECT_TRUE(base::PathExists(internal_shim_path_)); 204 EXPECT_TRUE(base::PathExists(shim_path_)); 205 206 // Create an extra shim in another folder. It should be deleted since its 207 // bundle id matches. 208 EXPECT_TRUE(shortcut_creator.BuildShortcut(other_shim_path)); 209 EXPECT_TRUE(base::PathExists(other_shim_path)); 210 211 // Change the user_data_dir of the shim at shim_path_. It should not be 212 // deleted since its user_data_dir does not match. 213 NSString* plist_path = base::mac::FilePathToNSString( 214 shim_path_.Append("Contents").Append("Info.plist")); 215 NSMutableDictionary* plist = 216 [NSDictionary dictionaryWithContentsOfFile:plist_path]; 217 [plist setObject:@"fake_user_data_dir" 218 forKey:app_mode::kCrAppModeUserDataDirKey]; 219 [plist writeToFile:plist_path 220 atomically:YES]; 221 222 EXPECT_TRUE(PathService::Override(chrome::DIR_USER_DATA, app_data_dir_)); 223 shortcut_creator.DeleteShortcuts(); 224 EXPECT_FALSE(base::PathExists(internal_shim_path_)); 225 EXPECT_TRUE(base::PathExists(shim_path_)); 226 EXPECT_FALSE(base::PathExists(other_shim_path)); 227 } 228 229 TEST_F(WebAppShortcutCreatorTest, CreateAppListShortcut) { 230 // With an empty |profile_name|, the shortcut path should not have the profile 231 // directory prepended to the extension id on the app bundle name. 232 info_.profile_name.clear(); 233 base::FilePath dst_path = 234 destination_dir_.Append(info_.extension_id + ".app"); 235 236 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(base::FilePath(), info_); 237 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 238 .WillRepeatedly(Return(destination_dir_)); 239 EXPECT_EQ(dst_path.BaseName(), shortcut_creator.GetShortcutBasename()); 240 } 241 242 TEST_F(WebAppShortcutCreatorTest, RunShortcut) { 243 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_, info_); 244 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 245 .WillRepeatedly(Return(destination_dir_)); 246 247 EXPECT_TRUE(shortcut_creator.CreateShortcuts( 248 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations())); 249 EXPECT_TRUE(base::PathExists(shim_path_)); 250 251 ssize_t status = getxattr( 252 shim_path_.value().c_str(), "com.apple.quarantine", NULL, 0, 0, 0); 253 EXPECT_EQ(-1, status); 254 EXPECT_EQ(ENOATTR, errno); 255 } 256 257 TEST_F(WebAppShortcutCreatorTest, CreateFailure) { 258 base::FilePath non_existent_path = 259 destination_dir_.Append("not-existent").Append("name.app"); 260 261 NiceMock<WebAppShortcutCreatorMock> shortcut_creator(app_data_dir_, info_); 262 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 263 .WillRepeatedly(Return(non_existent_path)); 264 EXPECT_FALSE(shortcut_creator.CreateShortcuts( 265 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations())); 266 } 267 268 TEST_F(WebAppShortcutCreatorTest, UpdateIcon) { 269 gfx::Image product_logo = 270 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed( 271 IDR_PRODUCT_LOGO_32); 272 info_.favicon.Add(product_logo); 273 WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_); 274 275 ASSERT_TRUE(shortcut_creator.UpdateIcon(shim_path_)); 276 base::FilePath icon_path = 277 shim_path_.Append("Contents").Append("Resources").Append("app.icns"); 278 279 base::scoped_nsobject<NSImage> image([[NSImage alloc] 280 initWithContentsOfFile:base::mac::FilePathToNSString(icon_path)]); 281 EXPECT_TRUE(image); 282 EXPECT_EQ(product_logo.Width(), [image size].width); 283 EXPECT_EQ(product_logo.Height(), [image size].height); 284 } 285 286 TEST_F(WebAppShortcutCreatorTest, RevealAppShimInFinder) { 287 WebAppShortcutCreatorMock shortcut_creator(app_data_dir_, info_); 288 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 289 .WillRepeatedly(Return(destination_dir_)); 290 291 EXPECT_CALL(shortcut_creator, RevealAppShimInFinder()) 292 .Times(0); 293 EXPECT_TRUE(shortcut_creator.CreateShortcuts( 294 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations())); 295 296 EXPECT_CALL(shortcut_creator, RevealAppShimInFinder()); 297 EXPECT_TRUE(shortcut_creator.CreateShortcuts( 298 SHORTCUT_CREATION_BY_USER, web_app::ShortcutLocations())); 299 } 300 301 TEST_F(WebAppShortcutCreatorTest, FileHandlers) { 302 CommandLine::ForCurrentProcess()->AppendSwitch( 303 switches::kEnableAppsFileAssociations); 304 extensions::FileHandlersInfo file_handlers_info; 305 extensions::FileHandlerInfo handler_0; 306 handler_0.extensions.insert("ext0"); 307 handler_0.extensions.insert("ext1"); 308 handler_0.types.insert("type0"); 309 handler_0.types.insert("type1"); 310 file_handlers_info.push_back(handler_0); 311 extensions::FileHandlerInfo handler_1; 312 handler_1.extensions.insert("ext2"); 313 handler_1.types.insert("type2"); 314 file_handlers_info.push_back(handler_1); 315 316 NiceMock<WebAppShortcutCreatorMock> shortcut_creator( 317 app_data_dir_, info_, file_handlers_info); 318 EXPECT_CALL(shortcut_creator, GetApplicationsDirname()) 319 .WillRepeatedly(Return(destination_dir_)); 320 EXPECT_TRUE(shortcut_creator.CreateShortcuts( 321 SHORTCUT_CREATION_AUTOMATED, web_app::ShortcutLocations())); 322 323 base::FilePath plist_path = 324 shim_path_.Append("Contents").Append("Info.plist"); 325 NSDictionary* plist = [NSDictionary 326 dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)]; 327 NSArray* file_handlers = 328 [plist objectForKey:app_mode::kCFBundleDocumentTypesKey]; 329 330 NSDictionary* file_handler_0 = [file_handlers objectAtIndex:0]; 331 EXPECT_NSEQ(app_mode::kBundleTypeRoleViewer, 332 [file_handler_0 objectForKey:app_mode::kCFBundleTypeRoleKey]); 333 NSArray* file_handler_0_extensions = 334 [file_handler_0 objectForKey:app_mode::kCFBundleTypeExtensionsKey]; 335 EXPECT_TRUE([file_handler_0_extensions containsObject:@"ext0"]); 336 EXPECT_TRUE([file_handler_0_extensions containsObject:@"ext1"]); 337 NSArray* file_handler_0_types = 338 [file_handler_0 objectForKey:app_mode::kCFBundleTypeMIMETypesKey]; 339 EXPECT_TRUE([file_handler_0_types containsObject:@"type0"]); 340 EXPECT_TRUE([file_handler_0_types containsObject:@"type1"]); 341 342 NSDictionary* file_handler_1 = [file_handlers objectAtIndex:1]; 343 EXPECT_NSEQ(app_mode::kBundleTypeRoleViewer, 344 [file_handler_1 objectForKey:app_mode::kCFBundleTypeRoleKey]); 345 NSArray* file_handler_1_extensions = 346 [file_handler_1 objectForKey:app_mode::kCFBundleTypeExtensionsKey]; 347 EXPECT_TRUE([file_handler_1_extensions containsObject:@"ext2"]); 348 NSArray* file_handler_1_types = 349 [file_handler_1 objectForKey:app_mode::kCFBundleTypeMIMETypesKey]; 350 EXPECT_TRUE([file_handler_1_types containsObject:@"type2"]); 351 } 352 353 } // namespace web_app 354