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
30using ::testing::_;
31using ::testing::Return;
32using ::testing::NiceMock;
33
34namespace {
35
36const char kFakeChromeBundleId[] = "fake.cfbundleidentifier";
37
38class 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
62web_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
73class 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
110namespace web_app {
111
112TEST_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
145TEST_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
181TEST_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
229TEST_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
242TEST_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
257TEST_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
268TEST_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
286TEST_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
301TEST_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