// 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 <map>
#include <string>
#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_preload_service/app_preload_service.h"
#include "chrome/browser/apps/app_preload_service/app_preload_service_factory.h"
#include "chrome/browser/apps/app_preload_service/proto/app_preload.pb.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/user_manager/scoped_user_manager.h"
#include "content/public/test/browser_test.h"
#include "net/dns/mock_host_resolver.h"
namespace apps {
namespace {
constexpr char kDefaultManifestUrl[] = "/manifest.json";
static constexpr char kFirstLoginFlowHistogramSuccessName[] =
"AppPreloadService.FirstLoginFlowTime.Success";
static constexpr char kFirstLoginFlowHistogramFailureName[] =
"AppPreloadService.FirstLoginFlowTime.Failure";
} // namespace
class AppPreloadServiceBrowserTest : public InProcessBrowserTest {
public:
AppPreloadServiceBrowserTest()
: startup_check_resetter_(
AppPreloadService::DisablePreloadsOnStartupForTesting()) {
feature_list_.InitWithFeatures(
{/*enabled_features=*/features::kAppPreloadService},
/*disabled_features=*/{});
AppPreloadServiceFactory::SkipApiKeyCheckForTesting(true);
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
https_server_.RegisterRequestHandler(base::BindRepeating(
&AppPreloadServiceBrowserTest::HandleRequest, base::Unretained(this)));
https_server_.AddDefaultHandlers(GetChromeTestDataDir());
ASSERT_TRUE(https_server_.Start());
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
ash::switches::kAlmanacApiUrl, https_server()->GetURL("/").spec());
// Icon URLs should remap to the test server.
host_resolver()->AddRule("meltingpot.googleusercontent.com", "127.0.0.1");
}
void TearDown() override {
AppPreloadServiceFactory::SkipApiKeyCheckForTesting(false);
}
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
const net::test_server::HttpRequest& request) {
if (manifest_responses_.contains(request.relative_url)) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("application/json");
response->set_content(manifest_responses_[request.relative_url]);
return response;
}
if (request.relative_url == "/v1/app-preload?alt=proto" &&
apps_proto_.has_value()) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("application/x-protobuf");
response->set_content(apps_proto_->SerializeAsString());
return response;
}
return nullptr;
}
std::string AddIconToManifest(const std::string& manifest_template) {
GURL icon_url = https_server()->GetURL("meltingpot.googleusercontent.com",
"/web_apps/blue-192.png");
constexpr char kIconsBlock[] = R"([{
"src": "$1",
"sizes": "192x192",
"type": "image/png"
}])";
std::string icon_value = base::ReplaceStringPlaceholders(
kIconsBlock, {icon_url.spec()}, nullptr);
return base::ReplaceStringPlaceholders(manifest_template, {icon_value},
nullptr);
}
// Sets the test server response for `relative_url` to the given JSON
// `manifest` string.
void SetManifestResponse(const std::string& relative_url,
const std::string& manifest) {
manifest_responses_[relative_url] = manifest;
}
void SetAppProvisioningResponse(proto::AppPreloadListResponse response) {
apps_proto_ = response;
}
Profile* profile() { return browser()->profile(); }
net::EmbeddedTestServer* https_server() { return &https_server_; }
AppRegistryCache& app_registry_cache() {
auto* proxy = AppServiceProxyFactory::GetForProfile(browser()->profile());
return proxy->AppRegistryCache();
}
private:
base::test::ScopedFeatureList feature_list_;
net::EmbeddedTestServer https_server_;
std::map<std::string, std::string> manifest_responses_;
std::optional<proto::AppPreloadListResponse> apps_proto_;
base::AutoReset<bool> startup_check_resetter_;
};
IN_PROC_BROWSER_TEST_F(AppPreloadServiceBrowserTest, OemWebAppInstall) {
base::HistogramTester histograms;
proto::AppPreloadListResponse response;
auto* app = response.add_apps_to_install();
app->set_name("Example App");
app->set_package_id("web:https://www.example.com/id");
app->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_OEM);
app->mutable_web_extras()->set_manifest_url(
https_server()->GetURL(kDefaultManifestUrl).spec());
app->mutable_web_extras()->set_original_manifest_url(
"https://www.example.com/");
const std::string kManifest = AddIconToManifest(R"({
"id": "id",
"name": "Example App",
"start_url": "/index.html",
"icons": $1
})");
SetAppProvisioningResponse(response);
SetManifestResponse(kDefaultManifestUrl, kManifest);
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_TRUE(result.Get());
auto app_id =
web_app::GenerateAppId("id", GURL("https://www.example.com/index.html"));
bool found =
app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {
EXPECT_EQ(update.Name(), "Example App");
EXPECT_EQ(update.InstallReason(), InstallReason::kOem);
EXPECT_EQ(update.PublisherId(), "https://www.example.com/index.html");
});
ASSERT_TRUE(found);
histograms.ExpectTotalCount(kFirstLoginFlowHistogramSuccessName, 1);
histograms.ExpectTotalCount(kFirstLoginFlowHistogramFailureName, 0);
}
IN_PROC_BROWSER_TEST_F(AppPreloadServiceBrowserTest, DefaultAppInstall) {
proto::AppPreloadListResponse response;
auto* app = response.add_apps_to_install();
app->set_name("Peanut Types");
app->set_package_id("web:https://peanuttypes.com/app");
app->set_install_reason(
proto::AppPreloadListResponse::INSTALL_REASON_DEFAULT);
app->mutable_web_extras()->set_manifest_url(
https_server()->GetURL(kDefaultManifestUrl).spec());
app->mutable_web_extras()->set_original_manifest_url(
"https://peanuttypes.com/app");
const std::string kManifest = AddIconToManifest(R"({
"name": "Example App",
"start_url": "/app",
"icons": $1
})");
SetManifestResponse(kDefaultManifestUrl, kManifest);
SetAppProvisioningResponse(response);
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_TRUE(result.Get());
auto app_id =
web_app::GenerateAppId(std::nullopt, GURL("https://peanuttypes.com/app"));
bool found =
app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {
EXPECT_EQ(update.InstallReason(), InstallReason::kDefault);
});
ASSERT_TRUE(found);
}
IN_PROC_BROWSER_TEST_F(AppPreloadServiceBrowserTest, IgnoreTestAppInstall) {
proto::AppPreloadListResponse response;
auto* app = response.add_apps_to_install();
app->set_name("Peanut Types");
app->set_package_id("web:https://peanuttypes.com/app");
app->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_TEST);
app->mutable_web_extras()->set_manifest_url(
https_server()->GetURL(kDefaultManifestUrl).spec());
app->mutable_web_extras()->set_original_manifest_url(
"https://peanuttypes.com/app");
SetAppProvisioningResponse(response);
// No call to SetManifestResponse, so if installation was attempted, it would
// fail.
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_TRUE(result.Get());
}
// Verifies that user-installed apps are not skipped, and are marked as OEM
// installed.
IN_PROC_BROWSER_TEST_F(AppPreloadServiceBrowserTest, InstallOverUserApp) {
constexpr char kResolvedManifestId[] = "https://www.example.com/manifest_id";
constexpr char kOriginalManifestUrl[] =
"https://www.example.com/manifest.json";
constexpr char kUserAppName[] = "User Installed App";
const std::string kManifest = AddIconToManifest(R"({
"id": "manifest_id",
"name": "OEM Installed app",
"start_url": "/",
"icons": $1
})");
auto app_id = web_app::test::InstallDummyWebApp(profile(), kUserAppName,
GURL(kResolvedManifestId));
proto::AppPreloadListResponse response;
auto* app = response.add_apps_to_install();
app->set_name("OEM Installed app");
app->set_package_id(base::StrCat({"web:", kResolvedManifestId}));
app->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_OEM);
app->mutable_web_extras()->set_manifest_url(
https_server()->GetURL(kDefaultManifestUrl).spec());
app->mutable_web_extras()->set_original_manifest_url(kOriginalManifestUrl);
SetAppProvisioningResponse(response);
SetManifestResponse(kDefaultManifestUrl, kManifest);
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_TRUE(result.Get());
bool found = AppServiceProxyFactory::GetForProfile(profile())
->AppRegistryCache()
.ForOneApp(app_id, [](const AppUpdate& update) {
EXPECT_EQ(update.InstallReason(), InstallReason::kOem);
});
ASSERT_TRUE(found);
}
// Verifies that multiple OEM apps can be installed at once.
IN_PROC_BROWSER_TEST_F(AppPreloadServiceBrowserTest, InstallMultipleOemApps) {
constexpr char kOriginalManifestUrl1[] = "https://www.foo.com/manifest.json";
constexpr char kOriginalManifestUrl2[] = "https://www.bar.com/manifest.json";
proto::AppPreloadListResponse response;
auto* app1 = response.add_apps_to_install();
app1->set_name("Foo");
app1->set_package_id("web:https://www.foo.com/");
app1->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_OEM);
app1->mutable_web_extras()->set_manifest_url(
https_server()->GetURL("/manifest/foo.json").spec());
app1->mutable_web_extras()->set_original_manifest_url(kOriginalManifestUrl1);
const std::string kManifest1 = AddIconToManifest(R"({
"name": "Foo",
"start_url": "/",
"icons": $1
})");
SetManifestResponse("/manifest/foo.json", kManifest1);
auto* app2 = response.add_apps_to_install();
app2->set_name("Bar");
app2->set_package_id("web:https://www.bar.com/");
app2->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_OEM);
app2->mutable_web_extras()->set_manifest_url(
https_server()->GetURL("/manifest/bar.json").spec());
app2->mutable_web_extras()->set_original_manifest_url(kOriginalManifestUrl2);
const std::string kManifest2 = AddIconToManifest(R"({
"name": "Bar",
"start_url": "/",
"icons": $1
})");
SetManifestResponse("/manifest/bar.json", kManifest2);
SetAppProvisioningResponse(response);
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_TRUE(result.Get());
auto app_id1 =
web_app::GenerateAppId(std::nullopt, GURL("https://www.foo.com/"));
bool found =
app_registry_cache().ForOneApp(app_id1, [](const AppUpdate& update) {
EXPECT_EQ(update.Name(), "Foo");
EXPECT_EQ(update.InstallReason(), InstallReason::kOem);
});
ASSERT_TRUE(found);
auto app_id2 =
web_app::GenerateAppId(std::nullopt, GURL("https://www.bar.com/"));
found = app_registry_cache().ForOneApp(app_id2, [](const AppUpdate& update) {
EXPECT_EQ(update.Name(), "Bar");
EXPECT_EQ(update.InstallReason(), InstallReason::kOem);
});
ASSERT_TRUE(found);
}
// Verifies that failed installations are retried on the next login flow, and
// already installed apps are ignored.
IN_PROC_BROWSER_TEST_F(AppPreloadServiceBrowserTest, RetryFailedApps) {
base::HistogramTester histograms;
constexpr char kOriginalManifestUrl1[] = "https://www.foo.com/manifest.json";
constexpr char kOriginalManifestUrl2[] = "https://www.bar.com/manifest.json";
proto::AppPreloadListResponse response;
auto* app1 = response.add_apps_to_install();
app1->set_name("Foo");
app1->set_package_id("web:https://www.foo.com/");
app1->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_OEM);
app1->mutable_web_extras()->set_manifest_url(
https_server()->GetURL("/manifest/foo.json").spec());
app1->mutable_web_extras()->set_original_manifest_url(kOriginalManifestUrl1);
const std::string kManifest1 = AddIconToManifest(R"({
"name": "Foo",
"start_url": "/",
"icons": $1
})");
auto* app2 = response.add_apps_to_install();
app2->set_name("Bar");
app2->set_package_id("web:https://www.bar.com/");
app2->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_OEM);
app2->mutable_web_extras()->set_manifest_url(
https_server()->GetURL("/manifest/bar.json").spec());
app2->mutable_web_extras()->set_original_manifest_url(kOriginalManifestUrl2);
const std::string kManifest2 = AddIconToManifest(R"({
"name": "Bar",
"start_url": "/",
"icons": $1
})");
SetAppProvisioningResponse(response);
// foo.json installs successfully but bar.json gives an error.
SetManifestResponse("/manifest/foo.json", kManifest1);
SetManifestResponse("/manifest/bar.json", "");
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_FALSE(result.Get());
histograms.ExpectTotalCount(kFirstLoginFlowHistogramSuccessName, 0);
histograms.ExpectTotalCount(kFirstLoginFlowHistogramFailureName, 1);
// bar.json should be retried, and will now succeed. foo.json is skipped
// (ignoring the error it would give), and so the whole flow is successful.
SetManifestResponse("/manifest/foo.json", "");
SetManifestResponse("/manifest/bar.json", kManifest2);
base::test::TestFuture<bool> result2;
service->StartFirstLoginFlowForTesting(result2.GetCallback());
ASSERT_TRUE(result2.Get());
// Both apps should now be installed.
auto app_id1 =
web_app::GenerateAppId(std::nullopt, GURL("https://www.foo.com/"));
bool found = app_registry_cache().ForOneApp(app_id1, [](const AppUpdate&) {});
ASSERT_TRUE(found);
auto app_id2 =
web_app::GenerateAppId(std::nullopt, GURL("https://www.bar.com/"));
found = app_registry_cache().ForOneApp(app_id2, [](const AppUpdate&) {});
ASSERT_TRUE(found);
histograms.ExpectTotalCount(kFirstLoginFlowHistogramSuccessName, 1);
histograms.ExpectTotalCount(kFirstLoginFlowHistogramFailureName, 1);
}
IN_PROC_BROWSER_TEST_F(AppPreloadServiceBrowserTest, InstallNoApp) {
proto::AppPreloadListResponse response;
SetAppProvisioningResponse(response);
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_TRUE(result.Get());
}
class AppPreloadServiceWithTestAppsBrowserTest
: public AppPreloadServiceBrowserTest {
private:
base::test::ScopedFeatureList feature_list_{kAppPreloadServiceEnableTestApps};
};
// When kAppPreloadServiceEnableTestApps is enabled, apps with the "test"
// install reason should be installed.
IN_PROC_BROWSER_TEST_F(AppPreloadServiceWithTestAppsBrowserTest,
InstallTestApp) {
proto::AppPreloadListResponse response;
auto* app = response.add_apps_to_install();
app->set_name("Peanut Types");
app->set_package_id("web:https://peanuttypes.com/app");
app->set_install_reason(proto::AppPreloadListResponse::INSTALL_REASON_TEST);
app->mutable_web_extras()->set_manifest_url(
https_server()->GetURL(kDefaultManifestUrl).spec());
app->mutable_web_extras()->set_original_manifest_url(
"https://peanuttypes.com/app");
const std::string kManifest = AddIconToManifest(R"({
"name": "Peanut Types",
"start_url": "/app",
"icons": $1
})");
SetAppProvisioningResponse(response);
SetManifestResponse(kDefaultManifestUrl, kManifest);
base::test::TestFuture<bool> result;
auto* service = AppPreloadService::Get(profile());
service->StartFirstLoginFlowForTesting(result.GetCallback());
ASSERT_TRUE(result.Get());
auto app_id =
web_app::GenerateAppId(std::nullopt, GURL("https://peanuttypes.com/app"));
bool found = app_registry_cache().ForOneApp(app_id, [](const AppUpdate&) {});
ASSERT_TRUE(found);
}
} // namespace apps