blob: 761105771d2055f3c9629c6023cd77688ad0bc61 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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/web_app_install_manager.h"
#include <memory>
#include "base/callback.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/nullable_string16.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind_test_util.h"
#include "chrome/browser/installable/fake_installable_manager.h"
#include "chrome/browser/installable/installable_data.h"
#include "chrome/browser/installable/installable_manager.h"
#include "chrome/browser/ssl/security_state_tab_helper.h"
#include "chrome/browser/web_applications/components/web_app_constants.h"
#include "chrome/browser/web_applications/components/web_app_icon_generator.h"
#include "chrome/browser/web_applications/test/test_data_retriever.h"
#include "chrome/browser/web_applications/test/test_file_utils.h"
#include "chrome/browser/web_applications/test/test_install_finalizer.h"
#include "chrome/browser/web_applications/test/test_web_app_database.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_install_finalizer.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/test/base/testing_profile.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/manifest/manifest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/gurl.h"
namespace web_app {
namespace {
bool ReadBitmap(FileUtilsWrapper* utils,
const base::FilePath& file_path,
SkBitmap* bitmap) {
std::string icon_data;
if (!utils->ReadFileToString(file_path, &icon_data))
return false;
return gfx::PNGCodec::Decode(
reinterpret_cast<const unsigned char*>(icon_data.c_str()),
icon_data.size(), bitmap);
}
constexpr int kIconSizes[] = {
icon_size::k32, icon_size::k64, icon_size::k48,
icon_size::k96, icon_size::k128, icon_size::k256,
};
bool ContainsOneIconOfEachSize(const WebApplicationInfo& web_app_info) {
for (int size_px : kIconSizes) {
int num_icons_for_size =
std::count_if(web_app_info.icons.begin(), web_app_info.icons.end(),
[&size_px](const WebApplicationInfo::IconInfo& icon) {
return icon.width == size_px && icon.height == size_px;
});
if (num_icons_for_size != 1)
return false;
}
return true;
}
void TestAcceptDialogCallback(
content::WebContents* initiator_web_contents,
std::unique_ptr<WebApplicationInfo> web_app_info,
ForInstallableSite for_installable_site,
InstallManager::WebAppInstallationAcceptanceCallback acceptance_callback) {
std::move(acceptance_callback).Run(true /*accept*/, std::move(web_app_info));
}
void TestDeclineDialogCallback(
content::WebContents* initiator_web_contents,
std::unique_ptr<WebApplicationInfo> web_app_info,
ForInstallableSite for_installable_site,
InstallManager::WebAppInstallationAcceptanceCallback acceptance_callback) {
std::move(acceptance_callback).Run(false /*accept*/, std::move(web_app_info));
}
} // namespace
class WebAppInstallManagerTest : public WebAppTest {
public:
void SetUp() override {
WebAppTest::SetUp();
database_ = std::make_unique<TestWebAppDatabase>();
registrar_ = std::make_unique<WebAppRegistrar>(database_.get());
auto file_utils = std::make_unique<TestFileUtils>();
file_utils_ = file_utils.get();
icon_manager_ =
std::make_unique<WebAppIconManager>(profile(), std::move(file_utils));
auto install_finalizer = std::make_unique<WebAppInstallFinalizer>(
registrar_.get(), icon_manager_.get());
install_manager_ = std::make_unique<WebAppInstallManager>(
profile(), std::move(install_finalizer));
}
void CreateRendererAppInfo(const GURL& url,
const std::string name,
const std::string description,
const GURL& scope,
base::Optional<SkColor> theme_color) {
auto web_app_info = std::make_unique<WebApplicationInfo>();
web_app_info->app_url = url;
web_app_info->title = base::UTF8ToUTF16(name);
web_app_info->description = base::UTF8ToUTF16(description);
web_app_info->scope = scope;
web_app_info->theme_color = theme_color;
auto data_retriever =
std::make_unique<TestDataRetriever>(std::move(web_app_info));
data_retriever_ = data_retriever.get();
install_manager_->SetDataRetrieverForTesting(std::move(data_retriever));
}
void CreateRendererAppInfo(const GURL& url,
const std::string name,
const std::string description) {
CreateRendererAppInfo(url, name, description, GURL(), base::nullopt);
}
void CreateDefaultInstallableManager() {
InstallableManager::CreateForWebContents(web_contents());
// Required by InstallableManager.
// Causes eligibility check to return NOT_FROM_SECURE_ORIGIN for GetData.
SecurityStateTabHelper::CreateForWebContents(web_contents());
}
static base::NullableString16 ToNullableUTF16(const std::string& str) {
return base::NullableString16(base::UTF8ToUTF16(str), false);
}
void SetInstallFinalizerForTesting() {
auto install_finalizer = std::make_unique<TestInstallFinalizer>();
install_finalizer_ = install_finalizer.get();
install_manager_->SetInstallFinalizerForTesting(
std::move(install_finalizer));
}
void SetIconsMapToRetrieve(IconsMap icons_map) {
CHECK(data_retriever_);
data_retriever_->SetIcons(std::move(icons_map));
}
AppId InstallWebApp() {
AppId app_id;
base::RunLoop run_loop;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestAcceptDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kSuccess, code);
app_id = installed_app_id;
run_loop.Quit();
}));
run_loop.Run();
return app_id;
}
protected:
std::unique_ptr<TestWebAppDatabase> database_;
std::unique_ptr<WebAppRegistrar> registrar_;
std::unique_ptr<WebAppIconManager> icon_manager_;
std::unique_ptr<WebAppInstallManager> install_manager_;
// Owned by install_manager_:
TestFileUtils* file_utils_ = nullptr;
TestDataRetriever* data_retriever_ = nullptr;
TestInstallFinalizer* install_finalizer_ = nullptr;
};
TEST_F(WebAppInstallManagerTest, InstallFromWebContents) {
EXPECT_TRUE(AreWebAppsUserInstallable(profile()));
const GURL url = GURL("https://example.com/path");
const std::string name = "Name";
const std::string description = "Description";
const GURL scope = GURL("https://example.com/scope");
const base::Optional<SkColor> theme_color = 0xAABBCCDD;
const AppId app_id = GenerateAppIdFromURL(url);
CreateRendererAppInfo(url, name, description, scope, theme_color);
CreateDefaultInstallableManager();
base::RunLoop run_loop;
bool callback_called = false;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestAcceptDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kSuccess, code);
EXPECT_EQ(app_id, installed_app_id);
callback_called = true;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(callback_called);
WebApp* web_app = registrar_->GetAppById(app_id);
EXPECT_NE(nullptr, web_app);
EXPECT_EQ(app_id, web_app->app_id());
EXPECT_EQ(name, web_app->name());
EXPECT_EQ(description, web_app->description());
EXPECT_EQ(url, web_app->launch_url());
EXPECT_EQ(scope, web_app->scope());
EXPECT_EQ(theme_color, web_app->theme_color());
}
TEST_F(WebAppInstallManagerTest, AlreadyInstalled) {
const GURL url = GURL("https://example.com/path");
const std::string name = "Name";
const std::string description = "Description";
const AppId app_id = GenerateAppIdFromURL(url);
CreateRendererAppInfo(url, name, description);
CreateDefaultInstallableManager();
const AppId installed_web_app = InstallWebApp();
EXPECT_EQ(app_id, installed_web_app);
// Second attempt.
CreateRendererAppInfo(url, name, description);
base::RunLoop run_loop;
bool callback_called = false;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestAcceptDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& already_installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kAlreadyInstalled, code);
EXPECT_EQ(app_id, already_installed_app_id);
callback_called = true;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(callback_called);
}
TEST_F(WebAppInstallManagerTest, GetWebApplicationInfoFailed) {
install_manager_->SetDataRetrieverForTesting(
std::make_unique<TestDataRetriever>(
std::unique_ptr<WebApplicationInfo>()));
CreateDefaultInstallableManager();
base::RunLoop run_loop;
bool callback_called = false;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestAcceptDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kGetWebApplicationInfoFailed, code);
EXPECT_EQ(AppId(), installed_app_id);
callback_called = true;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(callback_called);
}
TEST_F(WebAppInstallManagerTest, WebContentsDestroyed) {
CreateRendererAppInfo(GURL("https://example.com/path"), "Name",
"Description");
CreateDefaultInstallableManager();
base::RunLoop run_loop;
bool callback_called = false;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestAcceptDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kWebContentsDestroyed, code);
EXPECT_EQ(AppId(), installed_app_id);
callback_called = true;
run_loop.Quit();
}));
// Destroy WebContents.
DeleteContents();
EXPECT_EQ(nullptr, web_contents());
run_loop.Run();
EXPECT_TRUE(callback_called);
}
TEST_F(WebAppInstallManagerTest, InstallableCheck) {
const std::string renderer_description = "RendererDescription";
CreateRendererAppInfo(GURL("https://renderer.com/path"), "RendererName",
renderer_description,
GURL("https://renderer.com/scope"), 0x00);
const GURL manifest_start_url = GURL("https://example.com/start");
const AppId app_id = GenerateAppIdFromURL(manifest_start_url);
const std::string manifest_name = "Name from Manifest";
const GURL manifest_scope = GURL("https://example.com/scope");
const base::Optional<SkColor> manifest_theme_color = 0xAABBCCDD;
{
auto manifest = std::make_unique<blink::Manifest>();
manifest->short_name = ToNullableUTF16("Short Name from Manifest");
manifest->name = ToNullableUTF16(manifest_name);
manifest->start_url = manifest_start_url;
manifest->scope = manifest_scope;
manifest->theme_color = manifest_theme_color;
FakeInstallableManager::CreateForWebContentsWithManifest(
web_contents(), NO_ERROR_DETECTED, GURL("https://example.com/manifest"),
std::move(manifest));
}
base::RunLoop run_loop;
bool callback_called = false;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestAcceptDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kSuccess, code);
EXPECT_EQ(app_id, installed_app_id);
callback_called = true;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(callback_called);
WebApp* web_app = registrar_->GetAppById(app_id);
EXPECT_NE(nullptr, web_app);
// Manifest data overrides Renderer data, except |description|.
EXPECT_EQ(app_id, web_app->app_id());
EXPECT_EQ(manifest_name, web_app->name());
EXPECT_EQ(manifest_start_url, web_app->launch_url());
EXPECT_EQ(renderer_description, web_app->description());
EXPECT_EQ(manifest_scope, web_app->scope());
EXPECT_EQ(manifest_theme_color, web_app->theme_color());
}
TEST_F(WebAppInstallManagerTest, GetIcons) {
CreateRendererAppInfo(GURL("https://example.com/path"), "Name",
"Description");
CreateDefaultInstallableManager();
SetInstallFinalizerForTesting();
const GURL icon_url = GURL("https://example.com/app.ico");
const SkColor color = SK_ColorBLUE;
// Generate one icon as if it was downloaded.
IconsMap icons_map =
GenerateIconsMapWithOneIcon(icon_url, icon_size::k128, color);
SetIconsMapToRetrieve(std::move(icons_map));
InstallWebApp();
std::unique_ptr<WebApplicationInfo> web_app_info =
install_finalizer_->web_app_info();
// Make sure that icons have been generated for all sub sizes.
EXPECT_TRUE(ContainsOneIconOfEachSize(*web_app_info));
for (const WebApplicationInfo::IconInfo& icon : web_app_info->icons) {
EXPECT_FALSE(icon.data.drawsNothing());
EXPECT_EQ(color, icon.data.getColor(0, 0));
// All icons should have an empty url except the original one:
if (icon.url != icon_url) {
EXPECT_EQ(GURL(), icon.url);
}
}
}
TEST_F(WebAppInstallManagerTest, GetIcons_NoIconsProvided) {
CreateRendererAppInfo(GURL("https://example.com/path"), "Name",
"Description");
CreateDefaultInstallableManager();
SetInstallFinalizerForTesting();
IconsMap icons_map;
SetIconsMapToRetrieve(std::move(icons_map));
InstallWebApp();
std::unique_ptr<WebApplicationInfo> web_app_info =
install_finalizer_->web_app_info();
// Make sure that icons have been generated for all sizes.
EXPECT_TRUE(ContainsOneIconOfEachSize(*web_app_info));
for (const WebApplicationInfo::IconInfo& icon : web_app_info->icons) {
EXPECT_FALSE(icon.data.drawsNothing());
// Since all icons are generated, they should have an empty url.
EXPECT_TRUE(icon.url.is_empty());
}
}
TEST_F(WebAppInstallManagerTest, WriteDataToDisk) {
CreateRendererAppInfo(GURL("https://example.com/path"), "Name",
"Description");
CreateDefaultInstallableManager();
// TestingProfile creates temp directory if TestingProfile::path_ is empty
// (i.e. if TestingProfile::Builder::SetPath was not called by a test fixture)
const base::FilePath profile_dir = profile()->GetPath();
const base::FilePath web_apps_dir = profile_dir.AppendASCII("WebApps");
EXPECT_FALSE(file_utils_->DirectoryExists(web_apps_dir));
const SkColor color = SK_ColorGREEN;
const int original_icon_size_px = icon_size::k512;
// Generate one icon as if it was fetched from renderer.
{
WebApplicationInfo::IconInfo icon_info = GenerateIconInfo(
GURL("https://example.com/app.ico"), original_icon_size_px, color);
data_retriever_->web_app_info().icons.push_back(std::move(icon_info));
}
const AppId app_id = InstallWebApp();
EXPECT_TRUE(file_utils_->DirectoryExists(web_apps_dir));
const base::FilePath temp_dir = web_apps_dir.AppendASCII("Temp");
EXPECT_TRUE(file_utils_->DirectoryExists(temp_dir));
EXPECT_TRUE(file_utils_->IsDirectoryEmpty(temp_dir));
const base::FilePath app_dir = web_apps_dir.AppendASCII(app_id);
EXPECT_TRUE(file_utils_->DirectoryExists(app_dir));
const base::FilePath icons_dir = app_dir.AppendASCII("Icons");
EXPECT_TRUE(file_utils_->DirectoryExists(icons_dir));
std::set<int> written_sizes_px;
base::FileEnumerator enumerator(icons_dir, true, base::FileEnumerator::FILES);
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
EXPECT_TRUE(path.MatchesExtension(FILE_PATH_LITERAL(".png")));
SkBitmap bitmap;
EXPECT_TRUE(ReadBitmap(file_utils_, path, &bitmap));
EXPECT_EQ(bitmap.width(), bitmap.height());
const int size_px = bitmap.width();
EXPECT_EQ(0UL, written_sizes_px.count(size_px));
base::FilePath size_file_name;
size_file_name =
size_file_name.AppendASCII(base::StringPrintf("%i.png", size_px));
EXPECT_EQ(size_file_name, path.BaseName());
written_sizes_px.insert(size_px);
EXPECT_EQ(color, bitmap.getColor(0, 0));
}
EXPECT_EQ(base::size(kIconSizes) + 1UL, written_sizes_px.size());
for (int size_px : kIconSizes)
written_sizes_px.erase(size_px);
written_sizes_px.erase(original_icon_size_px);
EXPECT_TRUE(written_sizes_px.empty());
}
TEST_F(WebAppInstallManagerTest, WriteDataToDiskFailed) {
const GURL app_url = GURL("https://example.com/path");
CreateRendererAppInfo(app_url, "Name", "Description");
CreateDefaultInstallableManager();
IconsMap icons_map = GenerateIconsMapWithOneIcon(
GURL("https://example.com/app.ico"), icon_size::k512, SK_ColorBLUE);
SetIconsMapToRetrieve(std::move(icons_map));
const base::FilePath profile_dir = profile()->GetPath();
const base::FilePath web_apps_dir = profile_dir.AppendASCII("WebApps");
EXPECT_TRUE(file_utils_->CreateDirectory(web_apps_dir));
// Induce an error: Simulate "Disk Full" for writing icon files.
file_utils_->SetRemainingDiskSpaceSize(1024);
base::RunLoop run_loop;
bool callback_called = false;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestAcceptDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kWriteDataFailed, code);
EXPECT_EQ(AppId(), installed_app_id);
callback_called = true;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(callback_called);
const base::FilePath temp_dir = web_apps_dir.AppendASCII("Temp");
EXPECT_TRUE(file_utils_->DirectoryExists(temp_dir));
EXPECT_TRUE(file_utils_->IsDirectoryEmpty(temp_dir));
const AppId app_id = GenerateAppIdFromURL(app_url);
const base::FilePath app_dir = web_apps_dir.AppendASCII(app_id);
EXPECT_FALSE(file_utils_->DirectoryExists(app_dir));
}
TEST_F(WebAppInstallManagerTest, UserInstallDeclined) {
const GURL url = GURL("https://example.com/path");
const AppId app_id = GenerateAppIdFromURL(url);
CreateRendererAppInfo(url, "Name", "Description");
CreateDefaultInstallableManager();
base::RunLoop run_loop;
bool callback_called = false;
const bool force_shortcut_app = false;
install_manager_->InstallWebApp(
web_contents(), force_shortcut_app,
base::BindOnce(TestDeclineDialogCallback),
base::BindLambdaForTesting(
[&](const AppId& installed_app_id, InstallResultCode code) {
EXPECT_EQ(InstallResultCode::kUserInstallDeclined, code);
EXPECT_EQ(installed_app_id, AppId());
callback_called = true;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_TRUE(callback_called);
WebApp* web_app = registrar_->GetAppById(app_id);
EXPECT_EQ(nullptr, web_app);
}
// TODO(loyso): Convert more tests from bookmark_app_helper_unittest.cc
} // namespace web_app