1// Copyright 2013 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#include "chrome/browser/apps/app_shim/app_shim_listener.h"
6
7#include <unistd.h>
8
9#include "base/bind.h"
10#include "base/check.h"
11#include "base/files/file_path.h"
12#include "base/files/file_util.h"
13#include "base/mac/foundation_util.h"
14#include "base/macros.h"
15#include "base/optional.h"
16#include "base/path_service.h"
17#include "base/run_loop.h"
18#include "base/threading/thread_restrictions.h"
19#include "chrome/app_shim/app_shim_controller.h"
20#include "chrome/browser/apps/app_shim/app_shim_host_bootstrap_mac.h"
21#include "chrome/browser/apps/app_shim/test/app_shim_listener_test_api_mac.h"
22#include "chrome/browser/browser_process.h"
23#include "chrome/browser/browser_process_platform_part.h"
24#include "chrome/browser/profiles/profile.h"
25#include "chrome/browser/ui/browser.h"
26#include "chrome/common/chrome_paths.h"
27#include "chrome/common/mac/app_mode_common.h"
28#include "chrome/common/mac/app_shim.mojom.h"
29#include "chrome/test/base/in_process_browser_test.h"
30#include "components/version_info/version_info.h"
31#include "content/public/test/browser_test.h"
32#include "content/public/test/test_utils.h"
33#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
34#include "mojo/public/cpp/bindings/pending_receiver.h"
35#include "mojo/public/cpp/bindings/receiver.h"
36#include "mojo/public/cpp/bindings/remote.h"
37#include "mojo/public/cpp/platform/named_platform_channel.h"
38#include "mojo/public/cpp/platform/platform_channel.h"
39#include "mojo/public/cpp/system/isolated_connection.h"
40
41using OnShimConnectedCallback =
42    chrome::mojom::AppShimHostBootstrap::OnShimConnectedCallback;
43
44const char kTestAppMode[] = "test_app";
45
46// TODO(https://crbug.com/1042727): Fix test GURL scoping and remove this getter
47// function.
48GURL TestAppUrl() {
49  return GURL("https://example.com");
50}
51
52// A test version of the AppShimController mojo client in chrome_main_app_mode.
53class TestShimClient : public chrome::mojom::AppShim {
54 public:
55  TestShimClient();
56
57  // Friend accessor.
58  mojo::PlatformChannelEndpoint ConnectToBrowser(
59      const mojo::NamedPlatformChannel::ServerName& server_name) {
60    return AppShimController::ConnectToBrowser(server_name);
61  }
62
63  mojo::PendingReceiver<chrome::mojom::AppShimHost> GetHostReceiver() {
64    return std::move(host_receiver_);
65  }
66  OnShimConnectedCallback GetOnShimConnectedCallback() {
67    return base::BindOnce(&TestShimClient::OnShimConnectedDone,
68                          base::Unretained(this));
69  }
70
71  mojo::Remote<chrome::mojom::AppShimHostBootstrap>& host_bootstrap() {
72    return host_bootstrap_;
73  }
74
75  // chrome::mojom::AppShim implementation (not used in testing, but can be).
76  void CreateRemoteCocoaApplication(
77      mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
78          receiver) override {}
79  void CreateCommandDispatcherForWidget(uint64_t widget_id) override {}
80  void SetUserAttention(
81      chrome::mojom::AppShimAttentionType attention_type) override {}
82  void SetBadgeLabel(const std::string& badge_label) override {}
83  void UpdateProfileMenu(
84      std::vector<chrome::mojom::ProfileMenuItemPtr> profile_menu_items,
85      bool use_new_picker) override {}
86
87 private:
88  void OnShimConnectedDone(
89      chrome::mojom::AppShimLaunchResult result,
90      mojo::PendingReceiver<chrome::mojom::AppShim> app_shim_receiver) {
91    shim_receiver_.Bind(std::move(app_shim_receiver));
92  }
93
94  mojo::IsolatedConnection mojo_connection_;
95  mojo::Receiver<chrome::mojom::AppShim> shim_receiver_{this};
96  mojo::Remote<chrome::mojom::AppShimHost> host_;
97  mojo::PendingReceiver<chrome::mojom::AppShimHost> host_receiver_;
98  mojo::Remote<chrome::mojom::AppShimHostBootstrap> host_bootstrap_;
99
100  DISALLOW_COPY_AND_ASSIGN(TestShimClient);
101};
102
103TestShimClient::TestShimClient()
104    : host_receiver_(host_.BindNewPipeAndPassReceiver()) {
105  base::FilePath user_data_dir;
106  CHECK(base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir));
107
108  std::string name_fragment =
109      base::StringPrintf("%s.%s.%s", base::mac::BaseBundleID(),
110                         app_mode::kAppShimBootstrapNameFragment,
111                         base::MD5String(user_data_dir.value()).c_str());
112  mojo::PlatformChannelEndpoint endpoint = ConnectToBrowser(name_fragment);
113
114  mojo::ScopedMessagePipeHandle message_pipe =
115      mojo_connection_.Connect(std::move(endpoint));
116  host_bootstrap_ = mojo::Remote<chrome::mojom::AppShimHostBootstrap>(
117      mojo::PendingRemote<chrome::mojom::AppShimHostBootstrap>(
118          std::move(message_pipe), 0));
119}
120
121// Browser Test for AppShimListener to test IPC interactions.
122class AppShimListenerBrowserTest : public InProcessBrowserTest,
123                                   public AppShimHostBootstrap::Client,
124                                   public chrome::mojom::AppShimHost {
125 public:
126  AppShimListenerBrowserTest() = default;
127
128 protected:
129  // Wait for OnShimProcessConnected, then send a quit, and wait for the
130  // response. Used to test launch behavior.
131  void RunAndExitGracefully();
132
133  // InProcessBrowserTest overrides:
134  void SetUpOnMainThread() override;
135  void TearDownOnMainThread() override;
136
137  // AppShimHostBootstrap::Client:
138  void OnShimProcessConnected(
139      std::unique_ptr<AppShimHostBootstrap> bootstrap) override;
140
141  std::unique_ptr<TestShimClient> test_client_;
142  std::vector<base::FilePath> last_launch_files_;
143  base::Optional<chrome::mojom::AppShimLaunchType> last_launch_type_;
144
145 private:
146  // chrome::mojom::AppShimHost.
147  void FocusApp() override {}
148  void ReopenApp() override {}
149  void FilesOpened(const std::vector<base::FilePath>& files) override {}
150  void ProfileSelectedFromMenu(const base::FilePath& profile_path) override {}
151
152  std::unique_ptr<base::RunLoop> runner_;
153  mojo::Receiver<chrome::mojom::AppShimHost> receiver_{this};
154  mojo::Remote<chrome::mojom::AppShim> app_shim_;
155
156  int launch_count_ = 0;
157
158  DISALLOW_COPY_AND_ASSIGN(AppShimListenerBrowserTest);
159};
160
161void AppShimListenerBrowserTest::RunAndExitGracefully() {
162  runner_ = std::make_unique<base::RunLoop>();
163  EXPECT_EQ(0, launch_count_);
164  runner_->Run();  // Will stop in OnShimProcessConnected().
165  EXPECT_EQ(1, launch_count_);
166  test_client_.reset();
167}
168
169void AppShimListenerBrowserTest::SetUpOnMainThread() {
170  // Can't do this in the constructor, it needs a BrowserProcess.
171  AppShimHostBootstrap::SetClient(this);
172}
173
174void AppShimListenerBrowserTest::TearDownOnMainThread() {
175  AppShimHostBootstrap::SetClient(nullptr);
176}
177
178void AppShimListenerBrowserTest::OnShimProcessConnected(
179    std::unique_ptr<AppShimHostBootstrap> bootstrap) {
180  ++launch_count_;
181  receiver_.Bind(bootstrap->GetAppShimHostReceiver());
182  last_launch_type_ = bootstrap->GetLaunchType();
183  last_launch_files_ = bootstrap->GetLaunchFiles();
184
185  bootstrap->OnConnectedToHost(app_shim_.BindNewPipeAndPassReceiver());
186  runner_->Quit();
187}
188
189// Test regular launch, which would ask Chrome to launch the app.
190IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, LaunchNormal) {
191  test_client_.reset(new TestShimClient());
192  auto app_shim_info = chrome::mojom::AppShimInfo::New();
193  app_shim_info->profile_path = browser()->profile()->GetPath();
194  app_shim_info->app_id = kTestAppMode;
195  app_shim_info->app_url = TestAppUrl();
196  app_shim_info->launch_type = chrome::mojom::AppShimLaunchType::kNormal;
197  test_client_->host_bootstrap()->OnShimConnected(
198      test_client_->GetHostReceiver(), std::move(app_shim_info),
199      test_client_->GetOnShimConnectedCallback());
200  RunAndExitGracefully();
201  EXPECT_EQ(chrome::mojom::AppShimLaunchType::kNormal, last_launch_type_);
202  EXPECT_TRUE(last_launch_files_.empty());
203}
204
205// Test register-only launch, used when Chrome has already launched the app.
206IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, LaunchRegisterOnly) {
207  test_client_.reset(new TestShimClient());
208  auto app_shim_info = chrome::mojom::AppShimInfo::New();
209  app_shim_info->profile_path = browser()->profile()->GetPath();
210  app_shim_info->app_id = kTestAppMode;
211  app_shim_info->app_url = TestAppUrl();
212  app_shim_info->launch_type = chrome::mojom::AppShimLaunchType::kRegisterOnly;
213  test_client_->host_bootstrap()->OnShimConnected(
214      test_client_->GetHostReceiver(), std::move(app_shim_info),
215      test_client_->GetOnShimConnectedCallback());
216
217  RunAndExitGracefully();
218  EXPECT_EQ(chrome::mojom::AppShimLaunchType::kRegisterOnly,
219            *last_launch_type_);
220  EXPECT_TRUE(last_launch_files_.empty());
221}
222
223// Ensure bootstrap name registers.
224IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, PRE_ReCreate) {
225  test::AppShimListenerTestApi test_api(
226      g_browser_process->platform_part()->app_shim_listener());
227  EXPECT_TRUE(test_api.mach_acceptor());
228}
229
230// Ensure the bootstrap name can be re-created after a prior browser process has
231// quit.
232IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTest, ReCreate) {
233  test::AppShimListenerTestApi test_api(
234      g_browser_process->platform_part()->app_shim_listener());
235  EXPECT_TRUE(test_api.mach_acceptor());
236}
237
238// Tests for the files created by AppShimListener.
239class AppShimListenerBrowserTestSymlink : public AppShimListenerBrowserTest {
240 public:
241  AppShimListenerBrowserTestSymlink() {}
242
243 protected:
244  base::FilePath version_path_;
245
246 private:
247  bool SetUpUserDataDirectory() override;
248  void TearDownInProcessBrowserTestFixture() override;
249
250  DISALLOW_COPY_AND_ASSIGN(AppShimListenerBrowserTestSymlink);
251};
252
253bool AppShimListenerBrowserTestSymlink::SetUpUserDataDirectory() {
254  // Create an existing symlink. It should be replaced by AppShimListener.
255  base::FilePath user_data_dir;
256  EXPECT_TRUE(base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir));
257
258  // Create an invalid RunningChromeVersion file.
259  version_path_ =
260      user_data_dir.Append(app_mode::kRunningChromeVersionSymlinkName);
261  EXPECT_TRUE(base::CreateSymbolicLink(base::FilePath("invalid_version"),
262                                       version_path_));
263  return AppShimListenerBrowserTest::SetUpUserDataDirectory();
264}
265
266void AppShimListenerBrowserTestSymlink::TearDownInProcessBrowserTestFixture() {
267  // Check that created files have been deleted.
268  EXPECT_FALSE(base::PathExists(version_path_));
269}
270
271IN_PROC_BROWSER_TEST_F(AppShimListenerBrowserTestSymlink,
272                       RunningChromeVersionCorrectlyWritten) {
273  // Check that the RunningChromeVersion file is correctly written.
274  base::FilePath version;
275  EXPECT_TRUE(base::ReadSymbolicLink(version_path_, &version));
276  EXPECT_EQ(version_info::GetVersionNumber(), version.value());
277}
278