blob: 56e32411dd8eed33c97b414bb8217673536d8b7b [file] [log] [blame]
// Copyright 2023 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/isolated_web_app_install_command_helper.h"
#include <map>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_expected_support.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/error/unusable_swbn_file_error.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_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_fake_response_reader_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_signed_web_bundle_builder.h"
#include "chrome/browser/web_applications/test/mock_data_retriever.h"
#include "chrome/browser/web_applications/test/test_web_app_url_loader.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_info.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_contents/web_app_data_retriever.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/testing_profile.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/webapps/browser/web_contents/web_app_url_loader.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_task_environment.h"
#include "net/http/http_status_code.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 "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "url/gurl.h"
namespace web_app {
namespace {
using ::base::test::ErrorIs;
using ::base::test::HasValue;
using ::base::test::IsNotNullCallback;
using ::base::test::RunOnceCallback;
using ::base::test::ValueIs;
using ::testing::_;
using ::testing::DoAll;
using ::testing::Each;
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::ResultOf;
using ::testing::UnorderedElementsAre;
using ::testing::VariantWith;
using ::testing::WithArg;
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);
}
IwaSourceWithMode CreateDevProxySource(
std::string_view dev_mode_proxy_url = "http://default-proxy-url.org/") {
return IwaSourceProxy{url::Origin::Create(GURL(dev_mode_proxy_url))};
}
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";
return manifest;
}
GURL CreateDefaultManifestURL(const GURL& application_url) {
return application_url.Resolve("/manifest.webmanifest");
}
auto ReturnManifest(const blink::mojom::ManifestPtr& manifest,
const GURL& manifest_url,
webapps::InstallableStatusCode error_code =
webapps::InstallableStatusCode::NO_ERROR_DETECTED) {
constexpr int kCallbackArgumentIndex = 1;
return DoAll(
WithArg<kCallbackArgumentIndex>(
[](const WebAppDataRetriever::CheckInstallabilityCallback& callback) {
DCHECK(!callback.is_null());
}),
RunOnceCallback<kCallbackArgumentIndex>(
/*manifest=*/manifest.Clone(),
/*manifest_url=*/manifest_url,
/*valid_manifest_for_web_app=*/true, error_code));
}
std::unique_ptr<MockDataRetriever> CreateDefaultDataRetriever(
const GURL& application_url) {
std::unique_ptr<MockDataRetriever> fake_data_retriever =
std::make_unique<NiceMock<MockDataRetriever>>();
EXPECT_CALL(*fake_data_retriever, GetWebAppInstallInfo).Times(0);
return fake_data_retriever;
}
class IsolatedWebAppInstallCommandHelperTest : public ::testing::Test {
public:
void SetUp() override {
scoped_feature_list_.InitWithFeatures(
{features::kIsolatedWebApps, features::kIsolatedWebAppDevMode}, {});
}
TestingProfile* profile() const { return profile_.get(); }
content::WebContents& web_contents() {
if (web_contents_ == nullptr) {
web_contents_ = content::WebContents::Create(
content::WebContents::CreateParams(profile()));
}
return *web_contents_;
}
private:
content::BrowserTaskEnvironment browser_task_environment_;
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<TestingProfile> profile_ = []() {
TestingProfile::Builder builder;
#if BUILDFLAG(IS_CHROMEOS_LACROS)
builder.SetIsMainProfile(true);
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
return builder.Build();
}();
std::unique_ptr<content::WebContents> web_contents_;
};
using IsolatedWebAppInstallCommandHelperTrustAndSignaturesTest =
IsolatedWebAppInstallCommandHelperTest;
TEST_F(IsolatedWebAppInstallCommandHelperTrustAndSignaturesTest,
DevProxySucceeds) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->CheckTrustAndSignatures(CreateDevProxySource(), &*profile(),
future.GetCallback());
EXPECT_THAT(future.Get(), HasValue());
}
TEST_F(IsolatedWebAppInstallCommandHelperTrustAndSignaturesTest,
DevProxyFailsWhenDevModeIsDisabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(features::kIsolatedWebAppDevMode);
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->CheckTrustAndSignatures(CreateDevProxySource(), &*profile(),
future.GetCallback());
EXPECT_THAT(
future.Take(),
ErrorIs(HasSubstr("Isolated Web App Developer Mode is not enabled")));
}
class IsolatedWebAppInstallCommandHelperTrustAndSignaturesBundleTest
: public IsolatedWebAppInstallCommandHelperTrustAndSignaturesTest,
public ::testing::WithParamInterface<bool> {
public:
IsolatedWebAppInstallCommandHelperTrustAndSignaturesBundleTest()
: is_dev_mode_(GetParam()),
source_(IwaSourceBundleWithMode(
base::FilePath{FILE_PATH_LITERAL("/testing/path/to/a/bundle")},
/*dev_mode=*/is_dev_mode_)) {}
protected:
bool is_dev_mode_;
IwaSourceWithMode source_;
};
TEST_P(IsolatedWebAppInstallCommandHelperTrustAndSignaturesBundleTest,
SucceedsWhenThereIsNoError) {
IsolatedWebAppUrlInfo url_info = CreateEd25519IsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
std::make_unique<FakeResponseReaderFactory>(*profile(), base::ok()));
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->CheckTrustAndSignatures(source_, &*profile(),
future.GetCallback());
EXPECT_THAT(future.Get(), HasValue());
}
TEST_P(IsolatedWebAppInstallCommandHelperTrustAndSignaturesBundleTest,
ErrorsOnBundleError) {
IsolatedWebAppUrlInfo url_info = CreateEd25519IsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
std::make_unique<FakeResponseReaderFactory>(
*profile(),
base::unexpected(UnusableSwbnFileError(
UnusableSwbnFileError::Error::kMetadataParserVersionError,
"test error"))));
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->CheckTrustAndSignatures(source_, &*profile(),
future.GetCallback());
EXPECT_THAT(future.Take(), ErrorIs(HasSubstr("test error")));
}
TEST_P(IsolatedWebAppInstallCommandHelperTrustAndSignaturesBundleTest,
DoesNotInstallDevModeBundleWhenDevModeIsDisabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(features::kIsolatedWebAppDevMode);
IsolatedWebAppUrlInfo url_info = CreateEd25519IsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
std::make_unique<FakeResponseReaderFactory>(*profile(), base::ok()));
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->CheckTrustAndSignatures(source_, &*profile(),
future.GetCallback());
if (is_dev_mode_) {
EXPECT_THAT(
future.Take(),
ErrorIs(HasSubstr("Isolated Web App Developer Mode is not enabled")));
} else {
EXPECT_THAT(future.Get(), HasValue());
}
}
INSTANTIATE_TEST_SUITE_P(
All,
IsolatedWebAppInstallCommandHelperTrustAndSignaturesBundleTest,
::testing::Bool(),
[](::testing::TestParamInfo<bool> param_info) {
return param_info.param ? "DevModeBundle" : "InstalledBundle";
});
using IsolatedWebAppInstallCommandHelperStoragePartitionTest =
IsolatedWebAppInstallCommandHelperTest;
TEST_F(IsolatedWebAppInstallCommandHelperStoragePartitionTest,
CreateIfNotPresent) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
command_helper->CreateStoragePartitionIfNotPresent(*profile());
EXPECT_THAT(profile()->GetStoragePartition(
url_info.storage_partition_config(profile()),
/*can_create=*/false),
NotNull());
}
TEST_F(IsolatedWebAppInstallCommandHelperStoragePartitionTest,
CreateIfPresent) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
auto* partition = profile()->GetStoragePartition(
url_info.storage_partition_config(profile()),
/*can_create=*/true);
command_helper->CreateStoragePartitionIfNotPresent(*profile());
EXPECT_THAT(profile()->GetStoragePartition(
url_info.storage_partition_config(profile()),
/*can_create=*/false),
Eq(partition));
}
using IsolatedWebAppInstallCommandHelperLoadUrlTest =
IsolatedWebAppInstallCommandHelperTest;
TEST_F(IsolatedWebAppInstallCommandHelperLoadUrlTest,
URLLoaderIsCalledWithUrlGivenToTheInstallCommand) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
auto url_loader = std::make_unique<TestWebAppUrlLoader>();
url_loader->SetNextLoadUrlResult(
url_info.origin().GetURL().Resolve(
".well-known/_generated_install_page.html"),
webapps::WebAppUrlLoaderResult::kUrlLoaded);
std::optional<webapps::WebAppUrlLoader::UrlComparison> last_url_comparison =
std::nullopt;
url_loader->TrackLoadUrlCalls(base::BindLambdaForTesting(
[&](const GURL& unused_url, content::WebContents* unused_web_contents,
webapps::WebAppUrlLoader::UrlComparison url_comparison) {
last_url_comparison = url_comparison;
}));
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->LoadInstallUrl(CreateDevProxySource(), web_contents(),
*url_loader, future.GetCallback());
EXPECT_THAT(future.Get(), HasValue());
EXPECT_THAT(
last_url_comparison,
Eq(webapps::WebAppUrlLoader::UrlComparison::kIgnoreQueryParamsAndRef));
}
TEST_F(IsolatedWebAppInstallCommandHelperLoadUrlTest,
SetDevModeLocationBeforeUrlLoading) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
auto url_loader = std::make_unique<TestWebAppUrlLoader>();
url_loader->SetNextLoadUrlResult(
url_info.origin().GetURL().Resolve(
".well-known/_generated_install_page.html"),
webapps::WebAppUrlLoaderResult::kUrlLoaded);
std::optional<IwaSourceWithMode> source = std::nullopt;
url_loader->TrackLoadUrlCalls(base::BindLambdaForTesting(
[&](const GURL& unused_url, content::WebContents* web_contents,
webapps::WebAppUrlLoader::UrlComparison unused_url_comparison) {
source =
IsolatedWebAppPendingInstallInfo::FromWebContents(*web_contents)
.source();
}));
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->LoadInstallUrl(
IwaSourceProxy{
url::Origin::Create(GURL("http://some-testing-proxy-url.com/"))},
web_contents(), *url_loader, future.GetCallback());
EXPECT_THAT(future.Get(), HasValue());
EXPECT_THAT(source, Optional(Eq(IwaSourceProxy{url::Origin::Create(
GURL("http://some-testing-proxy-url.com/"))})));
}
TEST_F(IsolatedWebAppInstallCommandHelperLoadUrlTest,
SetInstalledBundleLocationBeforeUrlLoading) {
IsolatedWebAppUrlInfo url_info = CreateEd25519IsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
auto url_loader = std::make_unique<TestWebAppUrlLoader>();
url_loader->SetNextLoadUrlResult(
url_info.origin().GetURL().Resolve(
".well-known/_generated_install_page.html"),
webapps::WebAppUrlLoaderResult::kUrlLoaded);
std::optional<IwaSourceWithMode> source = std::nullopt;
url_loader->TrackLoadUrlCalls(base::BindLambdaForTesting(
[&](const GURL& unused_url, content::WebContents* web_contents,
webapps::WebAppUrlLoader::UrlComparison unused_url_comparison) {
source =
IsolatedWebAppPendingInstallInfo::FromWebContents(*web_contents)
.source();
}));
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->LoadInstallUrl(
IwaSourceBundleProdMode{
base::FilePath{FILE_PATH_LITERAL("/testing/path/to/a/bundle")}},
web_contents(), *url_loader, future.GetCallback());
EXPECT_THAT(future.Get(), HasValue());
EXPECT_THAT(source, Optional(Eq(IwaSourceBundleProdMode{base::FilePath{
FILE_PATH_LITERAL("/testing/path/to/a/bundle")}})));
}
TEST_F(IsolatedWebAppInstallCommandHelperLoadUrlTest, HandlesFailure) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
auto url_loader = std::make_unique<TestWebAppUrlLoader>();
url_loader->SetNextLoadUrlResult(
url_info.origin().GetURL().Resolve(
".well-known/_generated_install_page.html"),
webapps::WebAppUrlLoaderResult::kFailedErrorPageLoaded);
base::test::TestFuture<base::expected<void, std::string>> future;
command_helper->LoadInstallUrl(CreateDevProxySource(), web_contents(),
*url_loader, future.GetCallback());
EXPECT_THAT(future.Get(), ErrorIs(HasSubstr("FailedErrorPageLoaded")));
}
using IsolatedWebAppInstallCommandHelperRetrieveManifestTest =
IsolatedWebAppInstallCommandHelperTest;
TEST_F(IsolatedWebAppInstallCommandHelperRetrieveManifestTest,
ServiceWorkerIsNotRequired) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
std::unique_ptr<MockDataRetriever> fake_data_retriever =
CreateDefaultDataRetriever(url_info.origin().GetURL());
EXPECT_CALL(*fake_data_retriever,
CheckInstallabilityAndRetrieveManifest(_, _, _))
.WillOnce(
ReturnManifest(CreateDefaultManifest(url_info.origin().GetURL()),
CreateDefaultManifestURL(url_info.origin().GetURL())));
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, std::move(fake_data_retriever),
/*response_reader_factory=*/nullptr);
base::test::TestFuture<base::expected<
IsolatedWebAppInstallCommandHelper::ManifestAndUrl, std::string>>
future;
command_helper->CheckInstallabilityAndRetrieveManifest(web_contents(),
future.GetCallback());
EXPECT_THAT(future.Get(), HasValue());
}
TEST_F(IsolatedWebAppInstallCommandHelperRetrieveManifestTest,
FailsWhenAppIsNotInstallable) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
std::unique_ptr<MockDataRetriever> fake_data_retriever =
CreateDefaultDataRetriever(url_info.origin().GetURL());
ON_CALL(*fake_data_retriever, CheckInstallabilityAndRetrieveManifest)
.WillByDefault(
ReturnManifest(blink::mojom::Manifest::New(),
GURL{"http://test-url-example.com/manifest.json"},
webapps::InstallableStatusCode::NO_MANIFEST));
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, std::move(fake_data_retriever),
/*response_reader_factory=*/nullptr);
base::test::TestFuture<base::expected<
IsolatedWebAppInstallCommandHelper::ManifestAndUrl, std::string>>
future;
command_helper->CheckInstallabilityAndRetrieveManifest(web_contents(),
future.GetCallback());
EXPECT_THAT(future.Take(), ErrorIs(HasSubstr("App is not installable")));
}
TEST_F(IsolatedWebAppInstallCommandHelperRetrieveManifestTest,
FailsWhenAppIsInstallableButManifestIsNull) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
std::unique_ptr<MockDataRetriever> fake_data_retriever =
CreateDefaultDataRetriever(url_info.origin().GetURL());
ON_CALL(*fake_data_retriever, CheckInstallabilityAndRetrieveManifest)
.WillByDefault(ReturnManifest(
/*manifest=*/nullptr,
CreateDefaultManifestURL(url_info.origin().GetURL())));
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, std::move(fake_data_retriever),
/*response_reader_factory=*/nullptr);
base::test::TestFuture<base::expected<
IsolatedWebAppInstallCommandHelper::ManifestAndUrl, std::string>>
future;
command_helper->CheckInstallabilityAndRetrieveManifest(web_contents(),
future.GetCallback());
EXPECT_THAT(future.Take(), ErrorIs(HasSubstr("Manifest is null")));
}
struct InvalidVersionParam {
std::optional<std::u16string> version;
std::string error;
std::string test_name;
};
using IsolatedWebAppInstallCommandHelperValidateManifestTest =
IsolatedWebAppInstallCommandHelperTest;
class InstallIsolatedWebAppCommandHelperInvalidVersionTest
: public IsolatedWebAppInstallCommandHelperValidateManifestTest,
public ::testing::WithParamInterface<InvalidVersionParam> {};
TEST_P(InstallIsolatedWebAppCommandHelperInvalidVersionTest,
InstallationFails) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
std::unique_ptr<MockDataRetriever> fake_data_retriever =
CreateDefaultDataRetriever(url_info.origin().GetURL());
auto manifest = CreateDefaultManifest(url_info.origin().GetURL());
manifest->version = GetParam().version;
IsolatedWebAppInstallCommandHelper::ManifestAndUrl manifest_and_url(
std::move(manifest),
CreateDefaultManifestURL(url_info.origin().GetURL()));
base::expected<WebAppInstallInfo, std::string> result =
command_helper->ValidateManifestAndCreateInstallInfo(
/*expected_version=*/std::nullopt, std::move(manifest_and_url));
EXPECT_THAT(result, ErrorIs(HasSubstr(GetParam().error)));
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
InstallIsolatedWebAppCommandHelperInvalidVersionTest,
::testing::Values(
InvalidVersionParam{.version = std::nullopt,
.error = "`version` is not present",
.test_name = "NoVersion"},
InvalidVersionParam{
.version = u"\xD801",
.error = "Failed to convert manifest `version` from UTF16 to UTF8",
.test_name = "InvalidUtf8"},
InvalidVersionParam{.version = u"10abc",
.error = "Failed to parse `version`",
.test_name = "InvalidVersionFormat"}),
[](const ::testing::TestParamInfo<
InstallIsolatedWebAppCommandHelperInvalidVersionTest::ParamType>&
info) { return info.param.test_name; });
TEST_F(IsolatedWebAppInstallCommandHelperValidateManifestTest,
FailsWhenAppVersionDoesNotMatchExpectedVersion) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
base::expected<WebAppInstallInfo, std::string> result =
command_helper->ValidateManifestAndCreateInstallInfo(
base::Version("99.99.99"),
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
CreateDefaultManifest(url_info.origin().GetURL()),
CreateDefaultManifestURL(url_info.origin().GetURL())));
EXPECT_THAT(result,
ErrorIs(HasSubstr(
"does not match the version provided in the manifest")));
}
TEST_F(IsolatedWebAppInstallCommandHelperValidateManifestTest,
SucceedsWhenManifestIdIsEmpty) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
base::expected<WebAppInstallInfo, std::string> result =
command_helper->ValidateManifestAndCreateInstallInfo(
/*expected_version=*/std::nullopt,
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
CreateDefaultManifest(url_info.origin().GetURL()),
CreateDefaultManifestURL(url_info.origin().GetURL())));
EXPECT_THAT(result, HasValue());
}
TEST_F(IsolatedWebAppInstallCommandHelperValidateManifestTest,
FailsWhenManifestIdIsNotEmpty) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
blink::mojom::ManifestPtr manifest =
CreateDefaultManifest(url_info.origin().GetURL());
manifest->id = url_info.origin().GetURL().Resolve("/test-manifest-id");
base::expected<WebAppInstallInfo, std::string> result =
command_helper->ValidateManifestAndCreateInstallInfo(
/*expected_version=*/std::nullopt,
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
std::move(manifest),
CreateDefaultManifestURL(url_info.origin().GetURL())));
EXPECT_THAT(result, ErrorIs(HasSubstr(R"(Manifest `id` must be "/")")));
}
TEST_F(IsolatedWebAppInstallCommandHelperValidateManifestTest,
FailsWhenManifestScopeIsNotSlash) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
blink::mojom::ManifestPtr manifest =
CreateDefaultManifest(url_info.origin().GetURL());
manifest->scope = url_info.origin().GetURL().Resolve("/scope");
base::expected<WebAppInstallInfo, std::string> result =
command_helper->ValidateManifestAndCreateInstallInfo(
/*expected_version=*/std::nullopt,
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
std::move(manifest),
CreateDefaultManifestURL(url_info.origin().GetURL())));
EXPECT_THAT(result, ErrorIs(HasSubstr("Scope should resolve to the origin")));
}
TEST_F(IsolatedWebAppInstallCommandHelperValidateManifestTest,
ScopeIsResolvedToRootWhenManifestScopeIsSlash) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
blink::mojom::ManifestPtr manifest =
CreateDefaultManifest(url_info.origin().GetURL());
manifest->scope = url_info.origin().GetURL().Resolve("/");
base::expected<WebAppInstallInfo, std::string> result =
command_helper->ValidateManifestAndCreateInstallInfo(
/*expected_version=*/std::nullopt,
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
std::move(manifest),
CreateDefaultManifestURL(url_info.origin().GetURL())));
EXPECT_THAT(result, ValueIs(Field(&WebAppInstallInfo::scope,
Eq(url_info.origin().GetURL()))));
}
TEST_F(IsolatedWebAppInstallCommandHelperValidateManifestTest,
UntranslatedNameIsEmptyWhenNameAndShortNameAreNotPresent) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, CreateDefaultDataRetriever(url_info.origin().GetURL()),
/*response_reader_factory=*/nullptr);
blink::mojom::ManifestPtr manifest =
CreateDefaultManifest(url_info.origin().GetURL());
manifest->name = std::nullopt;
manifest->short_name = std::nullopt;
base::expected<WebAppInstallInfo, std::string> result =
command_helper->ValidateManifestAndCreateInstallInfo(
/*expected_version=*/std::nullopt,
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
std::move(manifest),
CreateDefaultManifestURL(url_info.origin().GetURL())));
EXPECT_THAT(result,
ErrorIs(HasSubstr(
"App manifest must have either 'name' or 'short_name'")));
}
class InstallIsolatedWebAppCommandHelperManifestIconsTest
: public IsolatedWebAppInstallCommandHelperTest {
public:
protected:
GURL kSomeTestApplicationUrl = GURL("http://manifest-test-url.com");
blink::mojom::ManifestPtr CreateManifest() const {
return CreateDefaultManifest(kSomeTestApplicationUrl);
}
blink::Manifest::ImageResource CreateImageResourceForAnyPurpose(
const GURL& image_src) {
blink::Manifest::ImageResource image;
image.type = u"image/png";
image.sizes.push_back(gfx::Size{kImageSize, kImageSize});
image.purpose = {
blink::mojom::ManifestImageResource_Purpose::ANY,
};
image.src = image_src;
return image;
}
constexpr static int kImageSize = 96;
};
TEST_F(InstallIsolatedWebAppCommandHelperManifestIconsTest,
ManifestIconIsDownloaded) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
kSomeTestApplicationUrl = url_info.origin().GetURL();
GURL img_url = url_info.origin().GetURL().Resolve("icon.png");
blink::mojom::ManifestPtr manifest = CreateManifest();
manifest->icons = {CreateImageResourceForAnyPurpose(img_url)};
std::map<GURL, std::vector<SkBitmap>> icons = {{
img_url,
{gfx::test::CreateBitmap(kImageSize, SK_ColorRED)},
}};
DownloadedIconsHttpResults http_result = {
{IconUrlWithSize::CreateForUnspecifiedSize(img_url),
net::HttpStatusCode::HTTP_OK},
};
std::unique_ptr<MockDataRetriever> fake_data_retriever =
CreateDefaultDataRetriever(kSomeTestApplicationUrl);
EXPECT_CALL(*fake_data_retriever,
GetIcons(_,
UnorderedElementsAre(
IconUrlWithSize::CreateForUnspecifiedSize(img_url)),
/*skip_page_favicons=*/true,
/*fail_all_if_any_fail=*/true, IsNotNullCallback()))
.WillOnce(RunOnceCallback<4>(IconsDownloadedResult::kCompleted,
std::move(icons), http_result));
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, std::move(fake_data_retriever),
/*response_reader_factory=*/nullptr);
ASSERT_OK_AND_ASSIGN(
auto install_info,
command_helper->ValidateManifestAndCreateInstallInfo(
std::nullopt,
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
std::move(manifest),
CreateDefaultManifestURL(kSomeTestApplicationUrl))));
base::test::TestFuture<base::expected<WebAppInstallInfo, std::string>> future;
command_helper->RetrieveIconsAndPopulateInstallInfo(
std::move(install_info), web_contents(), future.GetCallback());
auto result = future.Take();
EXPECT_THAT(result, HasValue());
std::map<SquareSizePx, SkBitmap> icon_bitmaps = result->icon_bitmaps.any;
EXPECT_THAT(result, ValueIs(Field(
&WebAppInstallInfo::icon_bitmaps,
Field(&IconBitmaps::any,
Each(Pair(_, ResultOf(
"bitmap.color.at.0.0",
[](const SkBitmap& bitmap) {
return bitmap.getColor(0, 0);
},
Eq(SK_ColorRED))))))));
EXPECT_THAT(
result,
ValueIs(Field(
"manifest_icons", &WebAppInstallInfo::manifest_icons,
UnorderedElementsAre(Field(&apps::IconInfo::url, Eq(img_url))))));
}
TEST_F(InstallIsolatedWebAppCommandHelperManifestIconsTest,
InstallationFailsWhenIconDownloadingFails) {
IsolatedWebAppUrlInfo url_info = CreateRandomIsolatedWebAppUrlInfo();
kSomeTestApplicationUrl = url_info.origin().GetURL();
GURL img_url = url_info.origin().GetURL().Resolve("icon.png");
blink::mojom::ManifestPtr manifest = CreateManifest();
manifest->icons = {CreateImageResourceForAnyPurpose(img_url)};
std::unique_ptr<MockDataRetriever> fake_data_retriever =
CreateDefaultDataRetriever(url_info.origin().GetURL());
EXPECT_CALL(*fake_data_retriever, GetIcons(_, _, _, _, IsNotNullCallback()))
.WillOnce(RunOnceCallback<4>(IconsDownloadedResult::kAbortedDueToFailure,
IconsMap{}, DownloadedIconsHttpResults{}));
auto command_helper = std::make_unique<IsolatedWebAppInstallCommandHelper>(
url_info, std::move(fake_data_retriever),
/*response_reader_factory=*/nullptr);
ASSERT_OK_AND_ASSIGN(
auto install_info,
command_helper->ValidateManifestAndCreateInstallInfo(
std::nullopt,
IsolatedWebAppInstallCommandHelper::ManifestAndUrl(
std::move(manifest),
CreateDefaultManifestURL(kSomeTestApplicationUrl))));
base::test::TestFuture<base::expected<WebAppInstallInfo, std::string>> future;
command_helper->RetrieveIconsAndPopulateInstallInfo(
std::move(install_info), web_contents(), future.GetCallback());
auto result = future.Take();
EXPECT_THAT(
result,
ErrorIs(HasSubstr("Error during icon downloading: AbortedDueToFailure")));
}
struct VerifyRelocationVisitor {
explicit VerifyRelocationVisitor(
base::FilePath profile_dir,
base::FilePath source_path,
IwaSourceBundleModeAndFileOp bundle_mode_and_file_op)
: profile_dir_(std::move(profile_dir)),
source_path_(std::move(source_path)),
bundle_mode_and_file_op_(bundle_mode_and_file_op) {}
void operator()(const IwaStorageOwnedBundle& location) {
// Owned bundles should be relocated to the profile's IWA directory.
base::FilePath path = location.GetPath(profile_dir_);
EXPECT_TRUE(base::PathExists(path));
switch (bundle_mode_and_file_op_) {
case IwaSourceBundleModeAndFileOp::kDevModeCopy:
case IwaSourceBundleModeAndFileOp::kProdModeCopy:
EXPECT_TRUE(base::PathExists(source_path_));
break;
case IwaSourceBundleModeAndFileOp::kDevModeMove:
case IwaSourceBundleModeAndFileOp::kProdModeMove:
EXPECT_FALSE(base::PathExists(source_path_));
break;
case IwaSourceBundleModeAndFileOp::kDevModeReference:
FAIL();
}
EXPECT_NE(path, source_path_);
EXPECT_EQ(path.DirName().DirName(), profile_dir_.Append(kIwaDirName));
EXPECT_EQ(path.BaseName(), base::FilePath(kMainSwbnFileName));
}
void operator()(const IwaStorageUnownedBundle& location) {
// Unowned bundles should not be relocated.
EXPECT_EQ(bundle_mode_and_file_op_,
IwaSourceBundleModeAndFileOp::kDevModeReference);
EXPECT_EQ(location.path(), source_path_);
EXPECT_TRUE(base::PathExists(location.path()));
}
void operator()(const IwaStorageProxy& location) { FAIL(); }
private:
base::FilePath profile_dir_;
base::FilePath source_path_;
IwaSourceBundleModeAndFileOp bundle_mode_and_file_op_;
};
struct VerifyCleanupVisitor {
explicit VerifyCleanupVisitor(base::FilePath profile_dir)
: profile_dir_(std::move(profile_dir)) {}
void operator()(const IwaStorageOwnedBundle& location) {
// Owned bundles should be cleaned up, including their parent directory.
base::FilePath path = location.GetPath(profile_dir_);
EXPECT_FALSE(base::PathExists(path));
EXPECT_FALSE(base::PathExists(path.DirName()));
}
void operator()(const IwaStorageUnownedBundle& location) {
// Unowned bundles should not be cleaned up.
EXPECT_TRUE(base::PathExists(location.path()));
}
void operator()(const IwaStorageProxy& location) { FAIL(); }
private:
base::FilePath profile_dir_;
};
class InstallIsolatedWebAppCommandHelperRelocationTest
: public ::testing::TestWithParam<IwaSourceBundleModeAndFileOp> {
public:
using RelocationResult =
base::expected<IsolatedWebAppStorageLocation, std::string>;
void SetUp() override {
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryDirInDir(
temp_dir.GetPath(), FILE_PATH_LITERAL("profile"), &profile_dir_));
// A directory where source files are stored.
ASSERT_TRUE(base::CreateTemporaryDirInDir(
temp_dir.GetPath(), FILE_PATH_LITERAL("src"), &src_dir_));
}
protected:
base::test::TaskEnvironment task_environment;
base::ScopedTempDir temp_dir;
base::FilePath profile_dir_;
base::FilePath src_dir_;
};
TEST_P(InstallIsolatedWebAppCommandHelperRelocationTest, NormalFlow) {
base::FilePath bundle;
ASSERT_TRUE(base::CreateTemporaryFileInDir(src_dir_, &bundle));
IwaSourceWithModeAndFileOp source{
IwaSourceBundleWithModeAndFileOp(bundle, GetParam())};
// Check that relocation works.
base::test::TestFuture<RelocationResult> future;
UpdateBundlePathAndCreateStorageLocation(profile_dir_, source,
future.GetCallback());
RelocationResult result = future.Take();
ASSERT_TRUE(result.has_value());
absl::visit(VerifyRelocationVisitor{profile_dir_, bundle, GetParam()},
result->variant());
// Check that cleanup works.
base::test::TestFuture<void> cleanup_future;
CleanupLocationIfOwned(profile_dir_, result.value(),
cleanup_future.GetCallback());
ASSERT_TRUE(cleanup_future.Wait());
absl::visit(VerifyCleanupVisitor{profile_dir_}, result->variant());
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
InstallIsolatedWebAppCommandHelperRelocationTest,
::testing::Values(IwaSourceBundleModeAndFileOp::kDevModeCopy,
IwaSourceBundleModeAndFileOp::kDevModeMove,
IwaSourceBundleModeAndFileOp::kProdModeCopy,
IwaSourceBundleModeAndFileOp::kProdModeMove,
IwaSourceBundleModeAndFileOp::kDevModeReference),
[](const testing::TestParamInfo<
InstallIsolatedWebAppCommandHelperRelocationTest::ParamType>& info) {
return base::ToString(info.param);
});
TEST(InstallIsolatedWebAppCommandHelperCleanupTest, CleanupNotOwned) {
base::test::TaskEnvironment task_environment;
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath profile_dir;
ASSERT_TRUE(base::CreateTemporaryDirInDir(
temp_dir.GetPath(), FILE_PATH_LITERAL("profile"), &profile_dir));
// Create a file that is not in the owned IWA directory.
base::FilePath bundle_path;
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &bundle_path));
// Trying to cleanup the location that is not owned.
IwaStorageUnownedBundle location{bundle_path};
base::test::TestFuture<void> cleanup_future;
CleanupLocationIfOwned(profile_dir, location, cleanup_future.GetCallback());
ASSERT_TRUE(cleanup_future.Wait());
// Not owned file should not be deleted.
EXPECT_TRUE(base::PathExists(bundle_path));
}
} // namespace
} // namespace web_app