chromium/chrome/browser/ash/child_accounts/time_limits/app_service_wrapper_unittest.cc

// Copyright 2019 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/ash/child_accounts/time_limits/app_service_wrapper.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "ash/components/arc/mojom/app.mojom.h"
#include "ash/components/arc/mojom/app_permissions.mojom.h"
#include "ash/components/arc/test/fake_app_instance.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/scoped_command_line.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_service_test.h"
#include "chrome/browser/ash/app_list/arc/arc_app_test.h"
#include "chrome/browser/ash/child_accounts/apps/app_test_utils.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_types.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_install_finalizer.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/testing_profile.h"
#include "components/app_constants/constants.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

using web_app::GenerateAppId;
using web_app::WebAppProvider;

namespace ash {
namespace app_time {

namespace {

constexpr char kArcPackage1[] = "com.example.app1";
constexpr char kArcApp1[] = "ArcApp1";
constexpr char kArcPackage2[] = "com.example.app2";
constexpr char kArcApp2[] = "ArcApp2";

constexpr char kExtensionAppUrl[] = "https://example.com/";
constexpr char kExtensionNameChrome[] = "Chrome";
constexpr char kExtensionNameA[] = "ExtensionA";

constexpr char kWebAppUrl1[] = "https://webappone.com/";
constexpr char kWebAppName1[] = "WebApp1";
constexpr char kWebAppUrl2[] = "https://webapptwo.com/";
constexpr char kWebAppName2[] = "WebApp2";

}  // namespace

class AppServiceWrapperTest : public ::testing::Test {
 public:
  class MockListener : public AppServiceWrapper::EventListener {
   public:
    MockListener() = default;
    MockListener(const MockListener&) = delete;
    MockListener& operator=(const MockListener&) = delete;
    ~MockListener() override = default;

    MOCK_METHOD1(OnAppInstalled, void(const AppId& app_id));
    MOCK_METHOD1(OnAppUninstalled, void(const AppId& app_id));
    MOCK_METHOD1(OnAppAvailable, void(const AppId& app_id));
    MOCK_METHOD1(OnAppBlocked, void(const AppId& app_id));
  };

 protected:
  AppServiceWrapperTest() = default;
  AppServiceWrapperTest(const AppServiceWrapperTest&) = delete;
  AppServiceWrapperTest& operator=(const AppServiceWrapperTest&) = delete;
  ~AppServiceWrapperTest() override = default;

  AppServiceWrapper& tested_wrapper() { return tested_wrapper_; }
  MockListener& test_listener() { return test_listener_; }

  // testing::Test:
  void SetUp() override {
    testing::Test::SetUp();

    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kDisableDefaultApps);

    extensions::TestExtensionSystem* extension_system(
        static_cast<extensions::TestExtensionSystem*>(
            extensions::ExtensionSystem::Get(&profile_)));
    extension_service_ = extension_system->CreateExtensionService(
        base::CommandLine::ForCurrentProcess(), base::FilePath(), false);
    extension_service_->Init();

    web_app::test::AwaitStartWebAppProviderAndSubsystems(&profile_);

    app_service_test_.SetUp(&profile_);
    arc_test_.SetUp(&profile_);
    task_environment_.RunUntilIdle();

    tested_wrapper_.AddObserver(&test_listener_);

    // Install Chrome.
    scoped_refptr<extensions::Extension> chrome = CreateExtension(
        app_constants::kChromeAppId, kExtensionNameChrome, kExtensionAppUrl);
    extension_service_->AddComponentExtension(chrome.get());
    task_environment_.RunUntilIdle();
  }

  void TearDown() override {
    tested_wrapper_.RemoveObserver(&test_listener_);
    arc_test_.TearDown();

    testing::Test::TearDown();
  }

  void RunUntilIdle() { task_environment_.RunUntilIdle(); }

  void SimulateAppInstalled(const AppId& app_id,
                            const std::string& app_name,
                            std::optional<std::string> url = std::nullopt) {
    if (app_id.app_type() == apps::AppType::kArc) {
      const std::string& package_name = app_id.app_id();
      arc_test_.AddPackage(CreateArcAppPackage(package_name)->Clone());
      std::vector<arc::mojom::AppInfoPtr> apps;
      apps.emplace_back(CreateArcAppInfo(package_name, app_name));
      arc_test_.app_instance()->SendPackageAppListRefreshed(package_name, apps);
      task_environment_.RunUntilIdle();
      return;
    }

    if (app_id.app_type() == apps::AppType::kChromeApp) {
      scoped_refptr<extensions::Extension> ext =
          CreateExtension(app_id.app_id(), app_name, url.value());
      extension_service_->AddExtension(ext.get());
      task_environment_.RunUntilIdle();
      return;
    }

    if (app_id.app_type() == apps::AppType::kWeb) {
      DCHECK(url.has_value());
      const webapps::AppId installed_app_id = web_app::test::InstallDummyWebApp(
          &profile_, app_name, GURL(url.value()),
          webapps::WebappInstallSource::EXTERNAL_DEFAULT);
      EXPECT_EQ(installed_app_id, app_id.app_id());
      task_environment_.RunUntilIdle();
      return;
    }
  }

  void SimulateAppUninstalled(const AppId& app_id) {
    if (app_id.app_type() == apps::AppType::kArc) {
      const std::string& package_name = app_id.app_id();
      arc_test_.app_instance()->UninstallPackage(package_name);
      task_environment_.RunUntilIdle();
      return;
    }

    if (app_id.app_type() == apps::AppType::kWeb) {
      base::RunLoop run_loop;
      WebAppProvider::GetForTest(&profile_)
          ->scheduler()
          .RemoveInstallManagementMaybeUninstall(
              app_id.app_id(), web_app::WebAppManagement::kDefault,
              webapps::WebappUninstallSource::kExternalPreinstalled,
              base::BindLambdaForTesting(
                  [&](webapps::UninstallResultCode code) {
                    EXPECT_EQ(code, webapps::UninstallResultCode::kAppRemoved);
                    run_loop.Quit();
                  }));
      run_loop.Run();
      task_environment_.RunUntilIdle();
      return;
    }

    if (app_id.app_type() == apps::AppType::kChromeApp ||
        app_id.app_type() == apps::AppType::kWeb) {
      extension_service_->UnloadExtension(
          app_id.app_id(), extensions::UnloadedExtensionReason::UNINSTALL);
      task_environment_.RunUntilIdle();
      return;
    }
  }

  void SimulateAppDisabled(const AppId& app_id,
                           const std::string& app_name,
                           bool disabled) {
    if (app_id.app_type() == apps::AppType::kArc) {
      const std::string& package_name = app_id.app_id();
      std::vector<arc::mojom::AppInfoPtr> apps;
      apps.emplace_back(CreateArcAppInfo(package_name, app_name))->suspended =
          disabled;
      arc_test_.app_instance()->SendPackageAppListRefreshed(package_name, apps);
      task_environment_.RunUntilIdle();
      return;
    }

    if (app_id.app_type() == apps::AppType::kWeb) {
      WebAppProvider::GetForTest(&profile_)->scheduler().SetAppIsDisabled(
          app_id.app_id(), disabled, base::DoNothing());
      task_environment_.RunUntilIdle();
      return;
    }

    if (app_id.app_type() == apps::AppType::kChromeApp ||
        app_id.app_type() == apps::AppType::kWeb) {
      if (disabled) {
        extension_service_->DisableExtension(
            app_id.app_id(),
            extensions::disable_reason::DISABLE_BLOCKED_BY_POLICY);
      } else {
        extension_service_->EnableExtension(app_id.app_id());
      }
      task_environment_.RunUntilIdle();
      return;
    }
  }

 private:
  base::test::ScopedCommandLine scoped_command_line_;
  content::BrowserTaskEnvironment task_environment_;

  TestingProfile profile_;
  apps::AppServiceTest app_service_test_;
  ArcAppTest arc_test_;

  raw_ptr<extensions::ExtensionService> extension_service_ = nullptr;

  AppServiceWrapper tested_wrapper_{&profile_};
  MockListener test_listener_;
};

// Tests GetInstalledApps() method.
TEST_F(AppServiceWrapperTest, GetInstalledApps) {
  // Chrome is the only 'preinstalled' app.
  const AppId chrome =
      AppId(apps::AppType::kChromeApp, app_constants::kChromeAppId);
  std::vector<AppId> installed_apps = tested_wrapper().GetInstalledApps();
  EXPECT_EQ(1u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, chrome));

  // Add ARC app.
  const AppId app1(apps::AppType::kArc, kArcPackage1);
  EXPECT_CALL(test_listener(), OnAppInstalled(app1)).Times(1);
  SimulateAppInstalled(app1, kArcApp1);

  // Add extension app. It will be ignored, because PATL does not support
  // extensions (with exception of Chrome) now.
  const AppId app2(
      apps::AppType::kChromeApp,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kExtensionAppUrl)));

  EXPECT_CALL(test_listener(), OnAppInstalled(app2)).Times(1);
  SimulateAppInstalled(app2, kExtensionNameA, kExtensionAppUrl);

  // Add web app.
  const AppId app3(
      apps::AppType::kWeb,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kWebAppUrl1)));
  EXPECT_CALL(test_listener(), OnAppInstalled(app3)).Times(1);
  SimulateAppInstalled(app3, kWebAppName1, kWebAppUrl1);

  // Expect, chrome, ARC app, hosted extension app and web app to be included.
  const std::vector<AppId> expected_apps = {chrome, app1, app2, app3};
  installed_apps = tested_wrapper().GetInstalledApps();
  ASSERT_EQ(4u, installed_apps.size());
  for (const auto& app : expected_apps) {
    EXPECT_TRUE(base::Contains(installed_apps, app));
  }
}

TEST_F(AppServiceWrapperTest, GetAppName) {
  const AppId chrome(apps::AppType::kChromeApp, app_constants::kChromeAppId);
  EXPECT_EQ(kExtensionNameChrome, tested_wrapper().GetAppName(chrome));

  const AppId app1(apps::AppType::kArc, kArcPackage1);
  EXPECT_CALL(test_listener(), OnAppInstalled(app1)).Times(1);
  SimulateAppInstalled(app1, kArcApp1);

  const AppId app2(
      apps::AppType::kChromeApp,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kExtensionAppUrl)));

  EXPECT_CALL(test_listener(), OnAppInstalled(app2)).Times(1);
  SimulateAppInstalled(app2, kExtensionNameA, kExtensionAppUrl);

  const AppId app3(
      apps::AppType::kWeb,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kWebAppUrl1)));
  EXPECT_CALL(test_listener(), OnAppInstalled(app3)).Times(1);
  SimulateAppInstalled(app3, kWebAppName1, kWebAppUrl1);

  EXPECT_EQ(kArcApp1, tested_wrapper().GetAppName(app1));
  EXPECT_EQ(kExtensionNameA, tested_wrapper().GetAppName(app2));
  EXPECT_EQ(kWebAppName1, tested_wrapper().GetAppName(app3));
}

// Tests installs and uninstalls of Arc apps.
TEST_F(AppServiceWrapperTest, ArcAppInstallation) {
  // Only Chrome installed.
  EXPECT_EQ(1u, tested_wrapper().GetInstalledApps().size());

  // Install first ARC app.
  const AppId app1(apps::AppType::kArc, kArcPackage1);
  EXPECT_CALL(test_listener(), OnAppInstalled(app1)).Times(1);
  SimulateAppInstalled(app1, kArcApp1);

  std::vector<AppId> installed_apps = tested_wrapper().GetInstalledApps();
  EXPECT_EQ(2u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, app1));

  // Install second ARC app.
  const AppId app2(apps::AppType::kArc, kArcPackage2);
  EXPECT_CALL(test_listener(), OnAppInstalled(app2)).Times(1);
  SimulateAppInstalled(app2, kArcApp2);

  installed_apps = tested_wrapper().GetInstalledApps();
  EXPECT_EQ(3u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, app2));

  // Uninstall first ARC app.
  EXPECT_CALL(test_listener(), OnAppUninstalled(app1)).Times(1);
  SimulateAppUninstalled(app1);

  installed_apps = tested_wrapper().GetInstalledApps();
  ASSERT_EQ(2u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, app2));
}

// Tests installs and uninstalls of web apps.
TEST_F(AppServiceWrapperTest, WebAppInstallation) {
  // Only Chrome installed.
  EXPECT_EQ(1u, tested_wrapper().GetInstalledApps().size());

  // Install first web app.
  const AppId app1(
      apps::AppType::kWeb,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kWebAppUrl1)));
  EXPECT_CALL(test_listener(), OnAppInstalled(app1)).Times(1);
  SimulateAppInstalled(app1, kWebAppName1, kWebAppUrl1);

  std::vector<AppId> installed_apps = tested_wrapper().GetInstalledApps();
  EXPECT_EQ(2u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, app1));

  // Install second web app.
  const AppId app2(
      apps::AppType::kWeb,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kWebAppUrl2)));
  EXPECT_CALL(test_listener(), OnAppInstalled(app2)).Times(1);
  SimulateAppInstalled(app2, kWebAppName2, kWebAppUrl2);

  installed_apps = tested_wrapper().GetInstalledApps();
  EXPECT_EQ(3u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, app2));

  // Uninstall first web app.
  EXPECT_CALL(test_listener(), OnAppUninstalled(app1)).Times(1);
  SimulateAppUninstalled(app1);

  installed_apps = tested_wrapper().GetInstalledApps();
  ASSERT_EQ(2u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, app2));
}

TEST_F(AppServiceWrapperTest, ArcAppDisabled) {
  // Install ARC app.
  const AppId app(apps::AppType::kArc, kArcPackage1);
  EXPECT_CALL(test_listener(), OnAppInstalled(app)).Times(1);
  SimulateAppInstalled(app, kArcApp1);

  // Make app disabled.
  EXPECT_CALL(test_listener(), OnAppBlocked(app)).Times(1);
  SimulateAppDisabled(app, kArcApp1, true);

  // Re-enable app.
  EXPECT_CALL(test_listener(), OnAppAvailable(app)).Times(1);
  SimulateAppDisabled(app, kArcApp1, false);
}

TEST_F(AppServiceWrapperTest, WebAppDisabled) {
  // Install web app.
  const AppId app(
      apps::AppType::kWeb,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kWebAppUrl1)));
  EXPECT_CALL(test_listener(), OnAppInstalled(app)).Times(1);
  SimulateAppInstalled(app, kWebAppName1, kWebAppUrl1);

  // Make app disabled.
  EXPECT_CALL(test_listener(), OnAppBlocked(app)).Times(1);
  SimulateAppDisabled(app, kWebAppName1, true /*disabled*/);

  // Re-enable app.
  EXPECT_CALL(test_listener(), OnAppAvailable(app)).Times(1);
  SimulateAppDisabled(app, kWebAppName1, false /*disabled*/);
}

// PATL v1 does not support 'extensions' other than Chrome.
TEST_F(AppServiceWrapperTest, IgnoreOtherExtensions) {
  const AppId chrome(apps::AppType::kChromeApp, app_constants::kChromeAppId);
  std::vector<AppId> installed_apps = tested_wrapper().GetInstalledApps();
  EXPECT_TRUE(base::Contains(installed_apps, chrome));

  const AppId app1(
      apps::AppType::kChromeApp,
      GenerateAppId(/*manifest_id=*/std::nullopt, GURL(kExtensionAppUrl)));
  EXPECT_CALL(test_listener(), OnAppInstalled(app1)).Times(1);
  SimulateAppInstalled(app1, kExtensionNameA, kExtensionAppUrl);

  installed_apps = tested_wrapper().GetInstalledApps();
  EXPECT_EQ(2u, installed_apps.size());
  EXPECT_TRUE(base::Contains(installed_apps, chrome));

  // TODO(yilkal): simulate install for non hosted extension apps (such as
  // platform extensions apps, normal extensions, theme extensions for this
  // test)
}

// TODO(agawronska): Add tests for ARC apps activity once crrev.com/c/1906614 is
// landed.

}  // namespace app_time
}  // namespace ash