blob: f08890e71c20cfac17c4f9fb2372c1f285a97c50 [file] [log] [blame]
// 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/web_applications/isolated_web_apps/install_isolated_web_app_command.h"
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/functional/overloaded.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_storage_location.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_validator.h"
#include "chrome/browser/web_applications/isolated_web_apps/pending_install_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_signed_web_bundle_builder.h"
#include "chrome/browser/web_applications/locks/lock.h"
#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
#include "chrome/browser/web_applications/test/fake_web_contents_manager.h"
#include "chrome/browser/web_applications/test/mock_data_retriever.h"
#include "chrome/browser/web_applications/test/web_app_icon_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/web_app_command_manager.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_icon_manager.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_features.h"
#include "components/web_package/signed_web_bundles/ed25519_public_key.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/web_package/web_bundle_builder.h"
#include "components/webapps/browser/installable/installable_logging.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/web_contents/web_app_url_loader.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-shared.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "url/gurl.h"
namespace web_app {
namespace {
using ::base::BucketsAre;
using ::base::test::ErrorIs;
using ::base::test::HasValue;
using ::base::test::IsNotNullCallback;
using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::AllOf;
using ::testing::DoAll;
using ::testing::Eq;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::IsFalse;
using ::testing::IsNull;
using ::testing::IsTrue;
using ::testing::NiceMock;
using ::testing::Not;
using ::testing::NotNull;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::ResultOf;
using ::testing::UnorderedElementsAre;
using ::testing::VariantWith;
using ::testing::WithArg;
constexpr std::string_view kManifestPath =
"/.well-known/_generated_install_page.html";
constexpr std::string_view kIconPath = "/icon.png";
IsolatedWebAppUrlInfo CreateRandomIsolatedWebAppUrlInfo() {
web_package::SignedWebBundleId signed_web_bundle_id =
web_package::SignedWebBundleId::CreateRandomForDevelopment();
return IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
signed_web_bundle_id);
}
IsolatedWebAppUrlInfo CreateEd25519IsolatedWebAppUrlInfo() {
web_package::SignedWebBundleId signed_web_bundle_id =
web_package::SignedWebBundleId::CreateForEd25519PublicKey(
web_package::Ed25519PublicKey::Create(
base::make_span(kTestPublicKey)));
return IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
signed_web_bundle_id);
}
IwaSourceProxy CreateDevProxySource() {
return IwaSourceProxy(
url::Origin::Create(GURL("http://default-proxy-url.org/")));
}
blink::mojom::ManifestPtr CreateDefaultManifest(const GURL& application_url) {
auto manifest = blink::mojom::Manifest::New();
manifest->id = application_url.DeprecatedGetOriginAsURL();
manifest->scope = application_url.Resolve("/");
manifest->start_url = application_url.Resolve("/testing-start-url.html");
manifest->display = DisplayMode::kStandalone;
manifest->short_name = u"test short manifest name";
manifest->version = u"1.0.0";
blink::Manifest::ImageResource icon;
icon.src = application_url.Resolve(kIconPath);
icon.purpose = {blink::mojom::ManifestImageResource_Purpose::ANY};
icon.type = u"image/png";
icon.sizes = {gfx::Size(256, 256)};
manifest->icons.push_back(icon);
return manifest;
}
GURL CreateDefaultManifestURL(const GURL& application_url) {
return application_url.Resolve("/manifest.webmanifest");
}
class InstallIsolatedWebAppCommandTest : public WebAppTest {
public:
InstallIsolatedWebAppCommandTest() {
scoped_feature_list_.InitWithFeatures(
{features::kIsolatedWebApps, features::kIsolatedWebAppDevMode}, {});
}
void SetUp() override {
WebAppTest::SetUp();
test::AwaitStartWebAppProviderAndSubsystems(profile());
}
WebAppRegistrar& web_app_registrar() {
return fake_provider().registrar_unsafe();
}
WebAppIconManager& web_app_icon_manager() {
return fake_provider().icon_manager();
}
FakeWebContentsManager& web_contents_manager() {
return static_cast<FakeWebContentsManager&>(
fake_provider().web_contents_manager());
}
std::pair<FakeWebContentsManager::FakePageState&,
FakeWebContentsManager::FakeIconState&>
SetUpPageAndIconStates(const IsolatedWebAppUrlInfo& url_info) {
GURL application_url = url_info.origin().GetURL();
auto& page_state = web_contents_manager().GetOrCreatePageState(
application_url.Resolve(kManifestPath));
page_state.url_load_result = webapps::WebAppUrlLoaderResult::kUrlLoaded;
page_state.error_code = webapps::InstallableStatusCode::NO_ERROR_DETECTED;
page_state.manifest_url = CreateDefaultManifestURL(application_url);
page_state.valid_manifest_for_web_app = true;
page_state.manifest_before_default_processing =
CreateDefaultManifest(application_url);
auto& icon_state = web_contents_manager().GetOrCreateIconState(
application_url.Resolve(kIconPath));
icon_state.bitmaps = {web_app::CreateSquareIcon(32, SK_ColorRED)};
return {page_state, icon_state};
}
struct Parameters {
IsolatedWebAppUrlInfo url_info;
std::optional<IsolatedWebAppInstallSource> install_source;
std::optional<base::Version> expected_version;
};
base::expected<InstallIsolatedWebAppCommandSuccess,
InstallIsolatedWebAppCommandError>
ExecuteCommand(Parameters parameters) {
base::test::TestFuture<base::expected<InstallIsolatedWebAppCommandSuccess,
InstallIsolatedWebAppCommandError>>
test_future;
fake_provider().scheduler().InstallIsolatedWebApp(
parameters.url_info,
parameters.install_source.value_or(
IsolatedWebAppInstallSource::FromDevUi(CreateDevProxySource())),
parameters.expected_version, /* optional_keep_alive=*/nullptr,
/*optional_profile_keep_alive=*/nullptr, test_future.GetCallback());
return test_future.Take();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(InstallIsolatedWebAppCommandTest, PropagateErrorWhenURLLoaderFails) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.url_load_result =
webapps::WebAppUrlLoaderResult::kFailedErrorPageLoaded;
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}),
ErrorIs(Field(&InstallIsolatedWebAppCommandError::message,
HasSubstr("Error during URL loading: "))));
}
TEST_F(InstallIsolatedWebAppCommandTest,
PropagateErrorWhenURLLoaderFailsWithDestroyedWebContentsError) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.url_load_result =
webapps::WebAppUrlLoaderResult::kFailedWebContentsDestroyed;
EXPECT_THAT(
ExecuteCommand(Parameters{.url_info = url_info}),
ErrorIs(Field(
&InstallIsolatedWebAppCommandError::message,
HasSubstr("Error during URL loading: FailedWebContentsDestroyed"))));
}
TEST_F(InstallIsolatedWebAppCommandTest,
InstallationSucceedesWhenFinalizerReturnSuccessNewInstall) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
}
TEST_F(InstallIsolatedWebAppCommandTest,
InstallationFailsWhenDevModeIsDisabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(features::kIsolatedWebAppDevMode);
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(
ExecuteCommand(Parameters{.url_info = url_info}),
ErrorIs(
Field(&InstallIsolatedWebAppCommandError::message,
HasSubstr("Isolated Web App Developer Mode is not enabled"))));
}
struct ProxyInstallSourceParam {
IsolatedWebAppInstallSource install_source;
WebAppManagement::Type expected_management_type;
};
class InstallIsolatedWebAppCommandProxyInstallSourceTest
: public InstallIsolatedWebAppCommandTest,
public ::testing::WithParamInterface<ProxyInstallSourceParam> {};
TEST_P(InstallIsolatedWebAppCommandProxyInstallSourceTest,
InstallationFinalizedWithCorrectInstallSurface) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(
ExecuteCommand(Parameters{.url_info = url_info,
.install_source = GetParam().install_source}),
HasValue());
const WebApp* web_app = web_app_registrar().GetAppById(url_info.app_id());
ASSERT_THAT(web_app, NotNull());
EXPECT_THAT(web_app->GetSources(),
Eq(WebAppManagementTypes{GetParam().expected_management_type}));
EXPECT_THAT(web_app->latest_install_source(),
Optional(Eq(GetParam().install_source.install_surface())));
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
InstallIsolatedWebAppCommandProxyInstallSourceTest,
::testing::Values(
ProxyInstallSourceParam{
.install_source =
IsolatedWebAppInstallSource::FromDevUi(CreateDevProxySource()),
.expected_management_type =
WebAppManagement::Type::kIwaUserInstalled},
ProxyInstallSourceParam{
.install_source = IsolatedWebAppInstallSource::FromDevCommandLine(
CreateDevProxySource()),
.expected_management_type =
WebAppManagement::Type::kIwaUserInstalled}));
TEST_F(InstallIsolatedWebAppCommandTest,
InstallationFailsWhenAppIsNotInstallable) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_url = GURL("http://test-url-example.com/manifest.json");
page_state.manifest_before_default_processing = blink::mojom::Manifest::New();
page_state.error_code = webapps::InstallableStatusCode::NO_MANIFEST;
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}),
ErrorIs(Field(&InstallIsolatedWebAppCommandError::message,
HasSubstr("App is not installable"))));
}
TEST_F(InstallIsolatedWebAppCommandTest, PendingUpdateInfoIsEmpty) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()),
Pointee(Property(
&WebApp::isolation_data,
Optional(Property(&WebApp::IsolationData::pending_update_info,
Eq(std::nullopt))))));
}
TEST_F(InstallIsolatedWebAppCommandTest,
InstallationFailsWhenAppVersionDoesNotMatchExpectedVersion) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(
ExecuteCommand(Parameters{.url_info = url_info,
.expected_version = base::Version("99.99.99")}),
ErrorIs(Field(
&InstallIsolatedWebAppCommandError::message,
HasSubstr("does not match the version provided in the manifest"))));
}
TEST_F(InstallIsolatedWebAppCommandTest, CommandLocksOnAppId) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
base::test::TestFuture<base::expected<InstallIsolatedWebAppCommandSuccess,
InstallIsolatedWebAppCommandError>>
test_future;
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, web_contents_manager().CreateDataRetriever(),
IsolatedWebAppInstallCommandHelper::CreateDefaultResponseReaderFactory(
*profile()));
auto command = std::make_unique<InstallIsolatedWebAppCommand>(
url_info, IsolatedWebAppInstallSource::FromDevUi(CreateDevProxySource()),
/*expected_version=*/std::nullopt,
content::WebContents::Create(
content::WebContents::CreateParams(profile())),
/*optional_keep_alive=*/nullptr,
/*optional_profile_keep_alive=*/nullptr, test_future.GetCallback(),
std::move(command_helper));
EXPECT_THAT(
command->InitialLockRequestForTesting(),
AllOf(Property(&LockDescription::type, Eq(LockDescription::Type::kApp)),
Property(&LockDescription::app_ids,
UnorderedElementsAre(url_info.app_id()))));
}
TEST_F(InstallIsolatedWebAppCommandTest, LocationSentToFinalizer) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(ExecuteCommand(Parameters{
.url_info = url_info,
.install_source = IsolatedWebAppInstallSource::FromDevUi(
IwaSourceProxy(url::Origin::Create(
GURL("http://some-testing-proxy-url.com/")))),
}),
HasValue());
EXPECT_THAT(
web_app_registrar().GetAppById(url_info.app_id()),
Pointee(AllOf(Property(
"isolation_data", &WebApp::isolation_data,
Optional(Field(
"location", &WebApp::IsolationData::location,
Property("variant", &IsolatedWebAppStorageLocation::variant,
VariantWith<IwaStorageProxy>(Property(
"proxy_url", &IwaStorageProxy::proxy_url,
Eq(url::Origin::Create(GURL(
"http://some-testing-proxy-url.com/"))))))))))));
}
TEST_F(InstallIsolatedWebAppCommandTest,
CreatesStorageParitionDuringInstallation) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(profile()->GetStoragePartition(
url_info.storage_partition_config(profile()),
/*can_create=*/false),
NotNull());
}
TEST_F(InstallIsolatedWebAppCommandTest, UsersCanDeleteIsolatedApp) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()),
Pointee(Property("CanUserUninstallWebApp",
&WebApp::CanUserUninstallWebApp, IsTrue())));
}
TEST_F(InstallIsolatedWebAppCommandTest,
CreatesStoragePartitionBeforeUrlLoading) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
content::StoragePartition* storage_partition_during_url_loading = nullptr;
web_contents_manager().TrackLoadUrlCalls(base::BindLambdaForTesting(
[&](content::NavigationController::LoadURLParams& load_url_params,
content::WebContents* unused_web_contents,
webapps::WebAppUrlLoader::UrlComparison unused_url_comparison) {
EXPECT_THAT(load_url_params.url,
Eq(url_info.origin().GetURL().Resolve(kManifestPath)));
storage_partition_during_url_loading = profile()->GetStoragePartition(
url_info.storage_partition_config(profile()),
/*can_create=*/false);
}));
EXPECT_THAT(profile()->GetStoragePartition(
url_info.storage_partition_config(profile()),
/*can_create=*/false),
IsNull());
EXPECT_THAT(ExecuteCommand({.url_info = url_info}), HasValue());
EXPECT_THAT(storage_partition_during_url_loading, NotNull());
}
using InstallIsolatedWebAppCommandManifestTest =
InstallIsolatedWebAppCommandTest;
TEST_F(InstallIsolatedWebAppCommandManifestTest,
PassesManifestIdToFinalizerWhenManifestIdIsEmpty) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()), NotNull());
}
TEST_F(InstallIsolatedWebAppCommandManifestTest,
FailsWhenManifestIdIsNotEmpty) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_before_default_processing->id =
url_info.origin().GetURL().Resolve("/test-manifest-id");
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}),
ErrorIs(Field(&InstallIsolatedWebAppCommandError::message,
HasSubstr(R"(Manifest `id` must be "/")"))));
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()), IsNull());
}
TEST_F(InstallIsolatedWebAppCommandManifestTest,
InstalledApplicationScopeIsResolvedToRootWhenManifestScopeIsSlash) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_before_default_processing->scope =
url_info.origin().GetURL().Resolve("/");
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()),
Pointee(Property("scope", &WebApp::scope,
Eq(url_info.origin().GetURL()))));
}
TEST_F(InstallIsolatedWebAppCommandManifestTest,
PassesManifestNameAsUntranslatedName) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_before_default_processing->name =
u"test application name";
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()),
Pointee(Property("untranslated_name", &WebApp::untranslated_name,
Eq("test application name"))));
}
TEST_F(InstallIsolatedWebAppCommandManifestTest,
UseShortNameAsUntranslatedNameWhenNameIsNotPresent) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_before_default_processing->name = std::nullopt;
page_state.manifest_before_default_processing->short_name =
u"test short name";
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()),
Pointee(Property("untranslated_name", &WebApp::untranslated_name,
Eq("test short name"))));
}
TEST_F(InstallIsolatedWebAppCommandManifestTest,
UseShortNameAsTitleWhenManifestNameIsEmpty) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_before_default_processing->name = u"";
page_state.manifest_before_default_processing->short_name =
u"other test short name";
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()),
Pointee(Property("untranslated_name", &WebApp::untranslated_name,
Eq("other test short name"))));
}
using InstallIsolatedWebAppCommandManifestIconsTest =
InstallIsolatedWebAppCommandManifestTest;
TEST_F(InstallIsolatedWebAppCommandManifestIconsTest,
ManifestIconIsDownloaded) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
base::test::TestFuture<std::map<SquareSizePx, SkBitmap>> test_future;
web_app_icon_manager().ReadIconAndResize(url_info.app_id(), IconPurpose::ANY,
SquareSizePx{1},
test_future.GetCallback());
std::map<SquareSizePx, SkBitmap> icon_bitmaps = test_future.Get();
EXPECT_THAT(icon_bitmaps,
UnorderedElementsAre(Pair(_, ResultOf(
"bitmap.color.at.0.0",
[](const SkBitmap& bitmap) {
return bitmap.getColor(0, 0);
},
Eq(SK_ColorRED)))));
EXPECT_THAT(web_app_registrar().GetAppById(url_info.app_id()),
Pointee(Property("manifest_icons", &WebApp::manifest_icons,
UnorderedElementsAre(_))));
}
TEST_F(InstallIsolatedWebAppCommandManifestIconsTest,
InstallationFailsWhenIconDownloadingFails) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
icon_state.http_status_code = net::HttpStatusCode::HTTP_NOT_FOUND;
icon_state.bitmaps = {};
EXPECT_THAT(
ExecuteCommand(Parameters{.url_info = url_info}),
ErrorIs(Field(
&InstallIsolatedWebAppCommandError::message,
HasSubstr("Error during icon downloading: AbortedDueToFailure"))));
}
using InstallIsolatedWebAppCommandMetricsTest =
InstallIsolatedWebAppCommandTest;
TEST_F(InstallIsolatedWebAppCommandMetricsTest,
ReportSuccessWhenFinishedSuccessfully) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
SetUpPageAndIconStates(url_info);
base::HistogramTester histogram_tester;
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}), HasValue());
EXPECT_THAT(histogram_tester.GetAllSamples("WebApp.Install.Result"),
BucketsAre(base::Bucket(true, 1)));
}
TEST_F(InstallIsolatedWebAppCommandMetricsTest, ReportErrorWhenUrlLoaderFails) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.url_load_result =
webapps::WebAppUrlLoaderResult::kFailedErrorPageLoaded;
base::HistogramTester histogram_tester;
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}),
Not(HasValue()));
EXPECT_THAT(histogram_tester.GetAllSamples("WebApp.Install.Result"),
BucketsAre(base::Bucket(false, 1)));
}
TEST_F(InstallIsolatedWebAppCommandMetricsTest,
ReportFailureWhenAppIsNotInstallable) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_url = GURL{"http://test-url-example.com/manifest.json"};
page_state.manifest_before_default_processing = blink::mojom::Manifest::New();
page_state.error_code = webapps::InstallableStatusCode::NO_MANIFEST;
base::HistogramTester histogram_tester;
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}),
Not(HasValue()));
EXPECT_THAT(histogram_tester.GetAllSamples("WebApp.Install.Result"),
BucketsAre(base::Bucket(false, 1)));
}
TEST_F(InstallIsolatedWebAppCommandMetricsTest,
ReportFailureWhenManifestIsNull) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_before_default_processing = nullptr;
page_state.error_code = webapps::InstallableStatusCode::NO_MANIFEST;
base::HistogramTester histogram_tester;
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}),
Not(HasValue()));
EXPECT_THAT(histogram_tester.GetAllSamples("WebApp.Install.Result"),
BucketsAre(base::Bucket(false, 1)));
}
TEST_F(InstallIsolatedWebAppCommandMetricsTest,
ReportFailureWhenManifestIdIsNotEmpty) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto [page_state, icon_state] = SetUpPageAndIconStates(url_info);
page_state.manifest_before_default_processing->id =
url_info.origin().GetURL().Resolve("/test manifest id");
base::HistogramTester histogram_tester;
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info}),
Not(HasValue()));
EXPECT_THAT(histogram_tester.GetAllSamples("WebApp.Install.Result"),
BucketsAre(base::Bucket(false, 1)));
}
struct BundleTestInfo {
bool has_error;
bool not_trusted;
bool want_success;
};
class InstallIsolatedWebAppCommandBundleTest
: public InstallIsolatedWebAppCommandTest,
public ::testing::WithParamInterface<std::tuple<bool, BundleTestInfo>> {
public:
InstallIsolatedWebAppCommandBundleTest()
: is_dev_mode_(std::get<0>(GetParam())),
bundle_info_(std::get<1>(GetParam())) {
if (is_dev_mode_) {
if (bundle_info_.not_trusted) {
// For a dev mode bundle to not be trusted, disable developer mode.
scoped_feature_list_.InitAndDisableFeature(
features::kIsolatedWebAppDevMode);
} else {
// Otherwise enable developer mode.
scoped_feature_list_.InitAndEnableFeature(
features::kIsolatedWebAppDevMode);
}
} else { // !is_dev_mode_
// Disable developer mode so that the bundle is not automatically trusted.
scoped_feature_list_.InitAndDisableFeature(
features::kIsolatedWebAppDevMode);
if (bundle_info_.not_trusted) {
SetTrustedWebBundleIdsForTesting({});
} else {
SetTrustedWebBundleIdsForTesting({url_info_.web_bundle_id()});
}
}
}
void SetUp() override {
base::FilePath bundle_path;
ASSERT_NO_FATAL_FAILURE(WriteWebBundle(bundle_path));
if (is_dev_mode_) {
install_source_ = IsolatedWebAppInstallSource::FromDevUi(
IwaSourceBundleDevModeWithFileOp(bundle_path,
kDefaultBundleDevFileOp));
} else {
install_source_ = IsolatedWebAppInstallSource::FromGraphicalInstaller(
IwaSourceBundleProdModeWithFileOp(bundle_path,
IwaSourceBundleProdFileOp::kCopy));
}
InstallIsolatedWebAppCommandTest::SetUp();
}
void WriteWebBundle(base::FilePath& bundle_path) {
ASSERT_THAT(temp_dir_.CreateUniqueTempDir(), IsTrue());
bundle_path = temp_dir_.GetPath().AppendASCII("bundle.swbn");
TestSignedWebBundleBuilder::BuildOptions build_options;
if (bundle_info_.has_error) {
build_options.SetErrorsForTesting(
{web_package::WebBundleSigner::ErrorForTesting::
kInvalidIntegrityBlockStructure});
}
TestSignedWebBundleBuilder builder;
TestSignedWebBundle bundle = builder.BuildDefault(std::move(build_options));
ASSERT_THAT(base::WriteFile(bundle_path, bundle.data), IsTrue());
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
base::ScopedTempDir temp_dir_;
bool is_dev_mode_;
BundleTestInfo bundle_info_;
std::optional<IsolatedWebAppInstallSource> install_source_;
IsolatedWebAppUrlInfo url_info_ = CreateEd25519IsolatedWebAppUrlInfo();
};
TEST_P(InstallIsolatedWebAppCommandBundleTest, InstallsWhenThereIsNoError) {
SetUpPageAndIconStates(url_info_);
auto result = ExecuteCommand(Parameters{
.url_info = url_info_,
.install_source = install_source_,
});
const base::FilePath iwa_root_dir = profile()->GetPath().Append(kIwaDirName);
if (bundle_info_.want_success) {
EXPECT_THAT(result, HasValue());
absl::visit(
base::Overloaded{[&iwa_root_dir](const IwaStorageOwnedBundle& bundle) {
EXPECT_TRUE(DirectoryExists(iwa_root_dir));
EXPECT_TRUE(PathExists(iwa_root_dir.AppendASCII(
bundle.dir_name_ascii())));
},
[&iwa_root_dir](const IwaStorageUnownedBundle&) {
EXPECT_FALSE(DirectoryExists(iwa_root_dir));
},
[](const IwaStorageProxy&) { FAIL(); }},
result->location.variant());
} else {
EXPECT_THAT(result, Not(HasValue()));
// Wait till IWA directory is removed.
task_environment()->RunUntilIdle();
absl::visit(
base::Overloaded{
[&iwa_root_dir](const IwaSourceBundleWithModeAndFileOp& source) {
switch (source.mode_and_file_op()) {
case IwaSourceBundleModeAndFileOp::kDevModeCopy:
case IwaSourceBundleModeAndFileOp::kDevModeMove:
case IwaSourceBundleModeAndFileOp::kProdModeCopy:
case IwaSourceBundleModeAndFileOp::kProdModeMove:
EXPECT_TRUE(DirectoryExists(iwa_root_dir));
EXPECT_TRUE(base::IsDirectoryEmpty(iwa_root_dir));
break;
case IwaSourceBundleModeAndFileOp::kDevModeReference:
EXPECT_FALSE(DirectoryExists(iwa_root_dir));
break;
}
},
[](const IwaSourceProxy&) { FAIL(); }},
install_source_->source().variant());
}
}
INSTANTIATE_TEST_SUITE_P(
All,
InstallIsolatedWebAppCommandBundleTest,
::testing::Combine(::testing::Bool(), // is_dev_mode
::testing::Values(BundleTestInfo{.has_error = false,
.not_trusted = false,
.want_success = true},
BundleTestInfo{.has_error = true,
.not_trusted = false,
.want_success = false},
BundleTestInfo{
.has_error = false,
.not_trusted = true,
.want_success = false})),
[](::testing::TestParamInfo<std::tuple<bool, BundleTestInfo>> param_info) {
return base::StrCat(
{std::get<0>(param_info.param) ? "DevModeBundle" : "InstalledBundle",
"_",
std::get<1>(param_info.param).has_error ? "has_error" : "no_error",
"_",
std::get<1>(param_info.param).not_trusted ? "not_trusted"
: "is_trusted"});
});
struct BundleInstallSourceParam {
base::FunctionRef<IsolatedWebAppInstallSource(const base::FilePath&)>
install_source;
WebAppManagement::Type expected_management_type;
};
class InstallIsolatedWebAppCommandBundleInstallSourceTest
: public InstallIsolatedWebAppCommandTest,
public ::testing::WithParamInterface<BundleInstallSourceParam> {
private:
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
};
TEST_P(InstallIsolatedWebAppCommandBundleInstallSourceTest,
InstallationFinalizedWithCorrectInstallSurface) {
IsolatedWebAppBuilder builder{ManifestBuilder()};
auto app = builder.BuildBundle(web_package::WebBundleSigner::KeyPair(
base::make_span(kTestPublicKey), base::make_span(kTestPrivateKey)));
app->FakeInstallPageState(profile());
app->TrustSigningKey();
IsolatedWebAppUrlInfo url_info = CreateEd25519IsolatedWebAppUrlInfo();
IsolatedWebAppInstallSource install_source =
GetParam().install_source(app->path());
EXPECT_THAT(ExecuteCommand(Parameters{.url_info = url_info,
.install_source = install_source}),
HasValue());
const WebApp* web_app = web_app_registrar().GetAppById(url_info.app_id());
ASSERT_THAT(web_app, NotNull());
EXPECT_THAT(web_app->GetSources(),
Eq(WebAppManagementTypes{GetParam().expected_management_type}));
EXPECT_THAT(web_app->latest_install_source(),
Optional(Eq(install_source.install_surface())));
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
InstallIsolatedWebAppCommandBundleInstallSourceTest,
::testing::Values(
BundleInstallSourceParam{
.install_source =
[](const base::FilePath& path) {
return IsolatedWebAppInstallSource::FromGraphicalInstaller(
IwaSourceBundleProdModeWithFileOp(
path, IwaSourceBundleProdFileOp::kCopy));
},
.expected_management_type =
WebAppManagement::Type::kIwaUserInstalled},
BundleInstallSourceParam{
.install_source =
[](const base::FilePath& path) {
return IsolatedWebAppInstallSource::FromExternalPolicy(
IwaSourceBundleProdModeWithFileOp(
path, IwaSourceBundleProdFileOp::kMove));
},
.expected_management_type = WebAppManagement::Type::kIwaPolicy},
BundleInstallSourceParam{
.install_source =
[](const base::FilePath& path) {
return IsolatedWebAppInstallSource::FromShimlessRma(
IwaSourceBundleProdModeWithFileOp(
path, IwaSourceBundleProdFileOp::kCopy));
},
.expected_management_type =
WebAppManagement::Type::kIwaShimlessRma},
BundleInstallSourceParam{
.install_source =
[](const base::FilePath& path) {
return IsolatedWebAppInstallSource::FromDevUi(
IwaSourceBundleDevModeWithFileOp(
path, IwaSourceBundleDevFileOp::kReference));
},
.expected_management_type =
WebAppManagement::Type::kIwaUserInstalled},
BundleInstallSourceParam{
.install_source =
[](const base::FilePath& path) {
return IsolatedWebAppInstallSource::FromDevCommandLine(
IwaSourceBundleDevModeWithFileOp(
path, IwaSourceBundleDevFileOp::kCopy));
},
.expected_management_type =
WebAppManagement::Type::kIwaUserInstalled}));
} // namespace
} // namespace web_app