blob: 89f6ee03eda95aab379f791e4a33d6aee491f58a [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include "base/functional/callback_helpers.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/web_app_browsertest_base.h"
#include "chrome/browser/ui/webui/web_app_internals/web_app_internals.mojom.h"
#include "chrome/browser/ui/webui/web_app_internals/web_app_internals_handler.h"
#include "chrome/browser/ui/webui/web_app_internals/web_app_internals_ui.h"
#include "chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_install_command_helper.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_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/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_install_manager.h"
#include "chrome/browser/web_applications/web_app_install_params.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/isolated_web_apps/types/update_channel.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace web_app {
namespace {
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::Pointee;
constexpr char kBadIconErrorTemplate[] = R"({
"!url": "$1banners/manifest_test_page.html",
"background_installation": false,
"install_surface": 15,
"stages": [ {
"!stage": "OnIconsRetrieved",
"icons_downloaded_result": "Completed",
"icons_http_results": [ {
"http_code_desc": "Not Found",
"http_status_code": 404,
"icon_size": "0x0",
"icon_url": "$1banners/bad_icon.png"
}, {
"http_code_desc": "Not Found",
"http_status_code": 404,
"icon_size": "0x0",
"icon_url": "$1favicon.ico"
} ],
"is_generated_icon": true
} ]
}
)";
// Drops all CR and LF characters.
std::string TrimLineEndings(std::string_view text) {
return base::CollapseWhitespaceASCII(
text,
/*trim_sequences_with_line_breaks=*/true);
}
} // namespace
class WebAppInternalsBrowserTest : public WebAppBrowserTestBase {
public:
WebAppInternalsBrowserTest() = default;
WebAppInternalsBrowserTest(const WebAppInternalsBrowserTest&) = delete;
WebAppInternalsBrowserTest& operator=(const WebAppInternalsBrowserTest&) =
delete;
~WebAppInternalsBrowserTest() override = default;
void SetUp() override {
embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir());
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&WebAppInternalsBrowserTest::RequestHandlerOverride,
base::Unretained(this)));
ASSERT_TRUE(embedded_test_server()->Start());
WebAppBrowserTestBase::SetUp();
}
void SetUpOnMainThread() override {
test::WaitUntilReady(WebAppProvider::GetForTest(browser()->profile()));
WebAppBrowserTestBase::SetUpOnMainThread();
}
webapps::AppId InstallWebApp(const GURL& app_url) {
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), app_url));
webapps::AppId app_id;
base::RunLoop run_loop;
GetProvider().scheduler().FetchManifestAndInstall(
webapps::WebappInstallSource::OMNIBOX_INSTALL_ICON,
browser()->tab_strip_model()->GetActiveWebContents()->GetWeakPtr(),
base::BindOnce(test::TestAcceptDialogCallback),
base::BindLambdaForTesting([&](const webapps::AppId& new_app_id,
webapps::InstallResultCode code) {
EXPECT_EQ(code, webapps::InstallResultCode::kSuccessNewInstall);
app_id = new_app_id;
run_loop.Quit();
}),
FallbackBehavior::kAllowFallbackDataAlways);
run_loop.Run();
return app_id;
}
WebAppProvider& GetProvider() {
return *WebAppProvider::GetForTest(browser()->profile());
}
std::unique_ptr<net::test_server::HttpResponse> RequestHandlerOverride(
const net::test_server::HttpRequest& request) {
if (request_override_)
return request_override_.Run(request);
return nullptr;
}
void OverrideHttpRequest(GURL url, net::HttpStatusCode http_status_code) {
request_override_ = base::BindLambdaForTesting(
[url = std::move(url),
http_status_code](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.GetURL() != url)
return nullptr;
auto http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(http_status_code);
return std::move(http_response);
});
}
private:
net::EmbeddedTestServer::HandleRequestCallback request_override_;
base::test::ScopedFeatureList scoped_feature_list_{
features::kRecordWebAppDebugInfo};
};
// There are 2 error logs being persisted here, one generated from the commands
// and one generated while parsing the manifest into a `WebAppInstallInfo`,
// which logs the invalid icon errors.
IN_PROC_BROWSER_TEST_F(WebAppInternalsBrowserTest,
PRE_InstallManagerErrorsPersist) {
OverrideHttpRequest(embedded_test_server()->GetURL("/banners/bad_icon.png"),
net::HTTP_NOT_FOUND);
webapps::AppId app_id = InstallWebApp(embedded_test_server()->GetURL(
"/banners/manifest_test_page.html?manifest=manifest_bad_icon.json"));
const WebApp* web_app = GetProvider().registrar_unsafe().GetAppById(app_id);
ASSERT_TRUE(web_app);
EXPECT_TRUE(web_app->is_generated_icon());
const std::string expected_error = base::ReplaceStringPlaceholders(
kBadIconErrorTemplate, {embedded_test_server()->base_url().spec()},
nullptr);
ASSERT_TRUE(GetProvider().install_manager().error_log());
ASSERT_EQ(2u, GetProvider().install_manager().error_log()->size());
const base::Value& error_log =
(*GetProvider().install_manager().error_log())[1];
EXPECT_TRUE(error_log.is_dict());
EXPECT_EQ(4u, error_log.GetDict().size());
EXPECT_EQ(TrimLineEndings(expected_error),
TrimLineEndings(error_log.DebugString()));
}
IN_PROC_BROWSER_TEST_F(WebAppInternalsBrowserTest,
InstallManagerErrorsPersist) {
test::WaitUntilReady(WebAppProvider::GetForTest(browser()->profile()));
ASSERT_TRUE(GetProvider().install_manager().error_log());
ASSERT_EQ(2u, GetProvider().install_manager().error_log()->size());
const base::Value& error_log =
(*GetProvider().install_manager().error_log())[1];
EXPECT_TRUE(error_log.is_dict());
EXPECT_EQ(4u, error_log.GetDict().size());
// Parses base url from the log: the port for embedded_test_server() changes
// on every test run.
const std::string* url_value = error_log.GetDict().FindString("!url");
ASSERT_TRUE(url_value);
GURL url{*url_value};
ASSERT_TRUE(url.is_valid());
const std::string expected_error = base::ReplaceStringPlaceholders(
kBadIconErrorTemplate, {url.GetWithEmptyPath().spec()}, nullptr);
EXPECT_EQ(TrimLineEndings(expected_error),
TrimLineEndings(error_log.DebugString()));
}
class WebAppInternalsIwaInstallationBrowserTest
: public IsolatedWebAppBrowserTestHarness {
protected:
WebAppInternalsHandler* OpenWebAppInternals() {
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), GURL("chrome://web-app-internals")));
return static_cast<WebAppInternalsUI*>(browser()
->tab_strip_model()
->GetActiveWebContents()
->GetWebUI()
->GetController())
->GetHandlerForTesting();
}
IsolatedWebAppUpdateServerMixin update_server_mixin_{&mixin_host_};
};
IN_PROC_BROWSER_TEST_F(WebAppInternalsIwaInstallationBrowserTest,
FetchUpdateManifestAndInstallIwaAndUpdate) {
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("1.0.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()));
auto* handler = OpenWebAppInternals();
GURL update_manifest_url = update_server_mixin_.GetUpdateManifestUrl(
test::GetDefaultEd25519WebBundleId());
base::test::TestFuture<::mojom::ParseUpdateManifestFromUrlResultPtr>
um_future;
handler->ParseUpdateManifestFromUrl(update_manifest_url,
um_future.GetCallback());
auto um_result = um_future.Take();
ASSERT_TRUE(um_result->is_update_manifest());
const auto& update_manifest = *um_result->get_update_manifest();
ASSERT_THAT(update_manifest,
Field(&::mojom::UpdateManifest::versions,
ElementsAre(Pointee(
Field(&::mojom::VersionEntry::version, Eq("1.0.0"))))));
const GURL& web_bundle_url = update_manifest.versions[0]->web_bundle_url;
base::test::TestFuture<::mojom::InstallIsolatedWebAppResultPtr>
install_future;
auto params = ::mojom::InstallFromBundleUrlParams::New();
params->web_bundle_url = web_bundle_url;
params->update_info = ::mojom::UpdateInfo::New(
update_manifest_url, UpdateChannel::default_channel().ToString(),
/*pinned_version=*/std::nullopt, /*allow_downgrades=*/false);
handler->InstallIsolatedWebAppFromBundleUrl(std::move(params),
install_future.GetCallback());
ASSERT_TRUE(install_future.Take()->is_success());
webapps::AppId app_id = IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
test::GetDefaultEd25519WebBundleId())
.app_id();
{
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("1.0.0"));
EXPECT_EQ(iwa.isolation_data()->update_manifest_url(), update_manifest_url);
EXPECT_EQ(iwa.isolation_data()->update_channel(),
UpdateChannel::default_channel());
}
// Run an update check on the same manifest.
{
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(update_future.Get(),
HasSubstr("app is already on the latest version"));
}
// Now add a new entry to the manifest and re-run the update check.
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("2.0.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()));
{
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(update_future.Get(), HasSubstr("Update to v2.0.0 successful"));
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("2.0.0"));
EXPECT_EQ(iwa.isolation_data()->update_manifest_url(), update_manifest_url);
EXPECT_EQ(iwa.isolation_data()->update_channel(),
UpdateChannel::default_channel());
}
// Set the channel to "beta" and verify that other fields of IsolationData
// stay intact.
auto beta_channel = *UpdateChannel::Create("beta");
{
base::test::TestFuture<bool> set_channel_future;
handler->SetUpdateChannelForIsolatedWebApp(
app_id, beta_channel.ToString(), set_channel_future.GetCallback());
EXPECT_TRUE(set_channel_future.Get());
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("2.0.0"));
EXPECT_EQ(iwa.isolation_data()->update_manifest_url(), update_manifest_url);
EXPECT_EQ(iwa.isolation_data()->update_channel(), beta_channel);
}
// Now add new entries with v2.1.0 for the `beta` channel and v2.2.0 for the
// `default` channel and force an update check.
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("2.1.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()),
/*update_channels=*/{{beta_channel}});
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("2.2.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()));
// The update logic must pick up the v2.1.0 for `beta` instead of a higher
// v2.2.0 for `default`.
{
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(update_future.Get(), HasSubstr("Update to v2.1.0 successful"));
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("2.1.0"));
EXPECT_EQ(iwa.isolation_data()->update_manifest_url(), update_manifest_url);
EXPECT_EQ(iwa.isolation_data()->update_channel(), beta_channel);
}
// Add v2.3.0 to `beta` channel, pin the app to v2.1.0. Expect no update.
{
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("2.3.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()),
/*update_channels=*/{{beta_channel}});
base::test::TestFuture<bool> set_pinned_version_future;
handler->SetPinnedVersionForIsolatedWebApp(
app_id, "2.1.0", set_pinned_version_future.GetCallback());
EXPECT_TRUE(set_pinned_version_future.Get());
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(
update_future.Get(),
HasSubstr("Update skipped: app is already on the latest version or the "
"updates are disabled due to set `pinned_version` field."));
}
// Add v2.4.0 to `beta` channel, pin the app to v2.3.0. Expect an update to
// v2.3.0.
{
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("2.4.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()),
/*update_channels=*/{{beta_channel}});
base::test::TestFuture<bool> set_pinned_version_future;
handler->SetPinnedVersionForIsolatedWebApp(
app_id, "2.3.0", set_pinned_version_future.GetCallback());
EXPECT_TRUE(set_pinned_version_future.Get());
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(update_future.Get(), HasSubstr("Update to v2.3.0 successful"));
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("2.3.0"));
EXPECT_EQ(iwa.isolation_data()->update_manifest_url(), update_manifest_url);
EXPECT_EQ(iwa.isolation_data()->update_channel(), beta_channel);
}
// Unpin the app. App should be updated to v2.4.0.
{
handler->ResetPinnedVersionForIsolatedWebApp(app_id);
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(update_future.Get(), HasSubstr("Update to v2.4.0 successful"));
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("2.4.0"));
}
// Pin to v2.3.0, allow downgrades. Expect update to v2.3.0.
{
handler->SetAllowDowngradesForIsolatedWebApp(true, app_id);
base::test::TestFuture<bool> set_pinned_version_future;
handler->SetPinnedVersionForIsolatedWebApp(
app_id, "2.3.0", set_pinned_version_future.GetCallback());
EXPECT_TRUE(set_pinned_version_future.Get());
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(update_future.Get(), HasSubstr("Update to v2.3.0 successful"));
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("2.3.0"));
}
}
IN_PROC_BROWSER_TEST_F(
WebAppInternalsIwaInstallationBrowserTest,
FetchUpdateManifestAndInstallIwaAndUpdateInvalidPinnedVersion) {
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("1.0.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()));
auto* handler = OpenWebAppInternals();
GURL update_manifest_url = update_server_mixin_.GetUpdateManifestUrl(
test::GetDefaultEd25519WebBundleId());
base::test::TestFuture<::mojom::ParseUpdateManifestFromUrlResultPtr>
um_future;
handler->ParseUpdateManifestFromUrl(update_manifest_url,
um_future.GetCallback());
auto um_result = um_future.Take();
ASSERT_TRUE(um_result->is_update_manifest());
const auto& update_manifest = *um_result->get_update_manifest();
ASSERT_THAT(update_manifest,
Field(&::mojom::UpdateManifest::versions,
ElementsAre(Pointee(
Field(&::mojom::VersionEntry::version, Eq("1.0.0"))))));
const GURL& web_bundle_url = update_manifest.versions[0]->web_bundle_url;
base::test::TestFuture<::mojom::InstallIsolatedWebAppResultPtr>
install_future;
auto params = ::mojom::InstallFromBundleUrlParams::New();
params->web_bundle_url = web_bundle_url;
params->update_info = ::mojom::UpdateInfo::New(
update_manifest_url, UpdateChannel::default_channel().ToString(),
/*pinned_version=*/std::nullopt, /*allow_downgrades=*/false);
handler->InstallIsolatedWebAppFromBundleUrl(std::move(params),
install_future.GetCallback());
ASSERT_TRUE(install_future.Take()->is_success());
webapps::AppId app_id = IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
test::GetDefaultEd25519WebBundleId())
.app_id();
// Add v2.0.0 to the update manifest.
update_server_mixin_.AddBundle(
IsolatedWebAppBuilder(ManifestBuilder().SetVersion("2.0.0"))
.BuildBundle(test::GetDefaultEd25519KeyPair()));
// Pin the app to an non-existent but valid version.
{
base::test::TestFuture<bool> set_pinned_version_future;
handler->SetPinnedVersionForIsolatedWebApp(
app_id, "1.1.1", set_pinned_version_future.GetCallback());
EXPECT_TRUE(set_pinned_version_future.Get());
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(
update_future.Get(),
HasSubstr(
"Update failed: Error::kPinnedVersionNotFoundInUpdateManifest"));
ASSERT_OK_AND_ASSIGN(
const WebApp& iwa,
GetIsolatedWebAppById(provider().registrar_unsafe(), app_id));
// Expect the app to stay at v1.0.0.
EXPECT_EQ(iwa.isolation_data()->version(), base::Version("1.0.0"));
}
// Fails to pin the app to invalid version.
{
base::test::TestFuture<bool> set_pinned_version_future;
handler->SetPinnedVersionForIsolatedWebApp(
app_id, "invalid_version", set_pinned_version_future.GetCallback());
EXPECT_THAT(set_pinned_version_future.Get(), 0);
base::test::TestFuture<std::string> update_future;
handler->UpdateManifestInstalledIsolatedWebApp(
app_id, update_future.GetCallback<const std::string&>());
EXPECT_THAT(
update_future.Get(),
HasSubstr(
"Update failed: Error::kPinnedVersionNotFoundInUpdateManifest"));
}
}
IN_PROC_BROWSER_TEST_F(WebAppInternalsIwaInstallationBrowserTest,
ParseUpdateManifestFromUrlFailsWithIncorrectUrl) {
auto* handler = OpenWebAppInternals();
base::test::TestFuture<::mojom::ParseUpdateManifestFromUrlResultPtr>
um_future;
// Select some dummy URL that certainly doesn't host an update manifest.
handler->ParseUpdateManifestFromUrl(GURL("https://example.com"),
um_future.GetCallback());
ASSERT_TRUE(um_future.Take()->is_error());
}
IN_PROC_BROWSER_TEST_F(
WebAppInternalsIwaInstallationBrowserTest,
InstallIsolatedWebAppFromBundleUrlFailsWithIncorrectUrl) {
auto* handler = OpenWebAppInternals();
base::test::TestFuture<::mojom::InstallIsolatedWebAppResultPtr>
install_future;
auto params = ::mojom::InstallFromBundleUrlParams::New();
// Select some dummy URL that certainly doesn't host a web bundle.
params->web_bundle_url = GURL("https://example.com");
handler->InstallIsolatedWebAppFromBundleUrl(std::move(params),
install_future.GetCallback());
ASSERT_TRUE(install_future.Take()->is_error());
}
} // namespace web_app