chromium/chrome/browser/chromeos/app_mode/chrome_kiosk_app_launcher_unittest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/chromeos/app_mode/chrome_kiosk_app_launcher.h"

#include <memory>
#include <vector>

#include "ash/test/ash_test_helper.h"
#include "base/memory/scoped_refptr.h"
#include "base/scoped_observation.h"
#include "base/test/scoped_command_line.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/app_service_test.h"
#include "chrome/browser/ash/app_mode/test_kiosk_extension_builder.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/ui/apps/chrome_app_delegate.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/test_app_window_contents.h"
#include "extensions/browser/test_event_router.h"
#include "extensions/common/api/app_runtime.h"

using base::test::TestFuture;
using extensions::Manifest;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::IsEmpty;
using LaunchResult = chromeos::ChromeKioskAppLauncher::LaunchResult;
using chromeos::ChromeKioskAppLauncher;

namespace ash {

namespace {

constexpr char kTestPrimaryAppId[] = "abcdefghabcdefghabcdefghabcdefgh";
constexpr char kSecondaryAppId[] = "aaaabbbbaaaabbbbaaaabbbbaaaabbbb";
constexpr char kExtraSecondaryAppId[] = "aaaaccccaaaaccccaaaaccccaaaacccc";

class AppLaunchTracker : public extensions::TestEventRouter::EventObserver {
 public:
  explicit AppLaunchTracker(extensions::TestEventRouter* event_router) {
    observation_.Observe(event_router);
  }
  AppLaunchTracker(const AppLaunchTracker&) = delete;
  AppLaunchTracker& operator=(const AppLaunchTracker&) = delete;

  std::vector<std::string> launched_apps() const { return launched_apps_; }

  // TestEventRouter::EventObserver:
  void OnBroadcastEvent(const extensions::Event& event) override {
    ADD_FAILURE() << "Unexpected broadcast " << event.event_name;
  }

  void OnDispatchEventToExtension(const std::string& extension_id,
                                  const extensions::Event& event) override {
    ASSERT_EQ(event.event_name,
              extensions::api::app_runtime::OnLaunched::kEventName);
    ASSERT_EQ(1u, event.event_args.size());

    const base::Value::Dict& launch_data = event.event_args[0].GetDict();
    std::optional<bool> is_kiosk_session =
        launch_data.FindBool("isKioskSession");
    ASSERT_TRUE(is_kiosk_session);
    EXPECT_TRUE(*is_kiosk_session);

    launched_apps_.push_back(extension_id);
  }

 private:
  const std::string app_id_;
  base::ScopedObservation<extensions::TestEventRouter,
                          extensions::TestEventRouter::EventObserver>
      observation_{this};
  std::vector<std::string> launched_apps_;
};

void InitAppWindow(extensions::AppWindow* app_window, const gfx::Rect& bounds) {
  // Create a TestAppWindowContents for the ShellAppDelegate to initialize the
  // ShellExtensionWebContentsObserver with.
  std::unique_ptr<content::WebContents> web_contents(
      content::WebContents::Create(
          content::WebContents::CreateParams(app_window->browser_context())));
  auto app_window_contents =
      std::make_unique<extensions::TestAppWindowContents>(
          std::move(web_contents));

  // Initialize the web contents and AppWindow.
  app_window->app_delegate()->InitWebContents(
      app_window_contents->GetWebContents());

  content::RenderFrameHost* main_frame =
      app_window_contents->GetWebContents()->GetPrimaryMainFrame();
  DCHECK(main_frame);

  extensions::AppWindow::CreateParams params;
  params.content_spec.bounds = bounds;
  app_window->Init(GURL(), std::move(app_window_contents), main_frame, params);
}

extensions::AppWindow* CreateAppWindow(Profile* profile,
                                       const extensions::Extension* extension,
                                       gfx::Rect bounds = {}) {
  extensions::AppWindow* app_window = new extensions::AppWindow(
      profile, std::make_unique<ChromeAppDelegate>(profile, true), extension);
  InitAppWindow(app_window, bounds);
  return app_window;
}

}  // namespace

class ChromeKioskAppLauncherTest : public extensions::ExtensionServiceTestBase,
                                   extensions::TestEventRouter::EventObserver {
 public:
  ChromeKioskAppLauncherTest()
      : extensions::ExtensionServiceTestBase(
            std::make_unique<content::BrowserTaskEnvironment>(
                content::BrowserTaskEnvironment::REAL_IO_THREAD)) {}

  // testing::Test:
  void SetUp() override {
    ash_test_helper_.SetUp(ash::AshTestHelper::InitParams());

    command_line_.GetProcessCommandLine()->AppendSwitch(
        switches::kForceAppMode);
    command_line_.GetProcessCommandLine()->AppendSwitch(switches::kAppId);

    extensions::ExtensionServiceTestBase::SetUp();
    InitializeEmptyExtensionService();

    apps::WaitForAppServiceProxyReady(
        apps::AppServiceProxyFactory::GetForProfile(profile()));

    extensions::TestEventRouter* event_router =
        extensions::CreateAndUseTestEventRouter(browser_context());
    app_launch_tracker_ = std::make_unique<AppLaunchTracker>(event_router);
  }

  void TearDown() override {
    app_launch_tracker_.reset();

    extensions::ExtensionServiceTestBase::TearDown();
    ash_test_helper_.TearDown();
  }

 protected:
  void CreateLauncher(bool is_network_ready) {
    launcher_ = std::make_unique<ChromeKioskAppLauncher>(
        profile(), kTestPrimaryAppId, is_network_ready);
  }

  void SimulateAppWindowLaunch(const extensions::Extension* extension) {
    CreateAppWindow(profile(), extension);
  }

  ash::AshTestHelper ash_test_helper_;
  std::unique_ptr<ChromeKioskAppLauncher> launcher_;
  std::unique_ptr<AppLaunchTracker> app_launch_tracker_;

 private:
  base::test::ScopedCommandLine command_line_;
};

TEST_F(ChromeKioskAppLauncherTest, ShouldFailIfPrimaryAppNotInstalled) {
  CreateLauncher(/*is_network_ready=*/true);

  TestFuture<LaunchResult> future;
  launcher_->LaunchApp(future.GetCallback());

  ASSERT_THAT(future.Get(), Eq(LaunchResult::kUnableToLaunch));
  ASSERT_THAT(app_launch_tracker_->launched_apps(), IsEmpty());
}

TEST_F(ChromeKioskAppLauncherTest, ShouldFailIfSecondaryAppNotInstalled) {
  TestKioskExtensionBuilder primary_app_builder(Manifest::TYPE_PLATFORM_APP,
                                                kTestPrimaryAppId);
  primary_app_builder.set_version("1.0");
  primary_app_builder.AddSecondaryExtension(kSecondaryAppId);
  scoped_refptr<const extensions::Extension> primary_app =
      primary_app_builder.Build();
  service()->AddExtension(primary_app.get());

  CreateLauncher(/*is_network_ready=*/true);

  TestFuture<LaunchResult> future;
  launcher_->LaunchApp(future.GetCallback());

  ASSERT_THAT(future.Get(), Eq(LaunchResult::kUnableToLaunch));
  ASSERT_THAT(app_launch_tracker_->launched_apps(), IsEmpty());
}

TEST_F(ChromeKioskAppLauncherTest,
       ShouldReportNetworkMissingIfAppNotOfflineEnabled) {
  TestKioskExtensionBuilder primary_app_builder(Manifest::TYPE_PLATFORM_APP,
                                                kTestPrimaryAppId);
  primary_app_builder.set_version("1.0");
  primary_app_builder.set_offline_enabled(false);
  scoped_refptr<const extensions::Extension> primary_app =
      primary_app_builder.Build();
  service()->AddExtension(primary_app.get());

  CreateLauncher(/*is_network_ready=*/false);

  TestFuture<LaunchResult> future;
  launcher_->LaunchApp(future.GetCallback());

  ASSERT_THAT(future.Get(), Eq(LaunchResult::kNetworkMissing));
  ASSERT_THAT(app_launch_tracker_->launched_apps(), IsEmpty());
}

TEST_F(ChromeKioskAppLauncherTest, ShouldSucceedIfNetworkAvailable) {
  TestKioskExtensionBuilder primary_app_builder(Manifest::TYPE_PLATFORM_APP,
                                                kTestPrimaryAppId);
  primary_app_builder.set_version("1.0");
  scoped_refptr<const extensions::Extension> primary_app =
      primary_app_builder.Build();
  service()->AddExtension(primary_app.get());

  CreateLauncher(/*is_network_ready=*/true);

  TestFuture<LaunchResult> future;
  launcher_->LaunchApp(future.GetCallback());

  SimulateAppWindowLaunch(primary_app.get());

  ASSERT_THAT(future.Get(), Eq(LaunchResult::kSuccess));

  EXPECT_THAT(app_launch_tracker_->launched_apps(),
              ElementsAre(kTestPrimaryAppId));
  EXPECT_TRUE(registry()->enabled_extensions().Contains(kTestPrimaryAppId));
}

TEST_F(ChromeKioskAppLauncherTest, ShouldSucceedWithSecondaryApp) {
  TestKioskExtensionBuilder primary_app_builder(Manifest::TYPE_PLATFORM_APP,
                                                kTestPrimaryAppId);
  primary_app_builder.set_version("1.0");
  primary_app_builder.AddSecondaryExtension(kSecondaryAppId);
  primary_app_builder.AddSecondaryExtensionWithEnabledOnLaunch(
      kExtraSecondaryAppId, false);
  scoped_refptr<const extensions::Extension> primary_app =
      primary_app_builder.Build();
  service()->AddExtension(primary_app.get());

  TestKioskExtensionBuilder secondary_app_builder(Manifest::TYPE_PLATFORM_APP,
                                                  kSecondaryAppId);
  secondary_app_builder.set_kiosk_enabled(false);
  scoped_refptr<const extensions::Extension> secondary_app =
      secondary_app_builder.Build();
  service()->AddExtension(secondary_app.get());

  TestKioskExtensionBuilder extra_secondary_app_builder(
      Manifest::TYPE_PLATFORM_APP, kExtraSecondaryAppId);
  extra_secondary_app_builder.set_kiosk_enabled(false);
  scoped_refptr<const extensions::Extension> extra_secondary_app =
      extra_secondary_app_builder.Build();
  service()->AddExtension(extra_secondary_app.get());

  CreateLauncher(/*is_network_ready=*/true);

  TestFuture<LaunchResult> future;
  launcher_->LaunchApp(future.GetCallback());

  SimulateAppWindowLaunch(primary_app.get());

  ASSERT_THAT(future.Get(), Eq(LaunchResult::kSuccess));

  EXPECT_THAT(app_launch_tracker_->launched_apps(),
              ElementsAre(kTestPrimaryAppId));
  EXPECT_TRUE(registry()->enabled_extensions().Contains(kTestPrimaryAppId));
  EXPECT_TRUE(registry()->enabled_extensions().Contains(kSecondaryAppId));
  EXPECT_TRUE(registry()->disabled_extensions().Contains(kExtraSecondaryAppId));
}

TEST_F(ChromeKioskAppLauncherTest, ShouldSucceedWithAppService) {
  TestKioskExtensionBuilder primary_app_builder(Manifest::TYPE_PLATFORM_APP,
                                                kTestPrimaryAppId);
  primary_app_builder.set_version("1.0");
  scoped_refptr<const extensions::Extension> primary_app =
      primary_app_builder.Build();
  service()->AddExtension(primary_app.get());

  CreateLauncher(/*is_network_ready=*/true);

  TestFuture<LaunchResult> future;
  launcher_->LaunchApp(future.GetCallback());

  SimulateAppWindowLaunch(primary_app.get());

  ASSERT_THAT(future.Get(), Eq(LaunchResult::kSuccess));

  EXPECT_THAT(app_launch_tracker_->launched_apps(),
              ElementsAre(kTestPrimaryAppId));
  EXPECT_TRUE(registry()->enabled_extensions().Contains(kTestPrimaryAppId));
}

}  // namespace ash