Home | History | Annotate | Download | only in web_applications
      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