// Copyright 2015 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/offline_pages/offline_page_utils.h"
#include <stdint.h>
#include <optional>
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/files/file.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/location.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/offline_pages/offline_page_model_factory.h"
#include "chrome/browser/offline_pages/offline_page_tab_helper.h"
#include "chrome/browser/offline_pages/request_coordinator_factory.h"
#include "chrome/browser/offline_pages/test_offline_page_model_builder.h"
#include "chrome/browser/offline_pages/test_request_coordinator_builder.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/test/base/testing_profile.h"
#include "components/offline_pages/core/background/request_coordinator.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/model/offline_page_model_taskified.h"
#include "components/offline_pages/core/offline_clock.h"
#include "components/offline_pages/core/offline_page_feature.h"
#include "components/offline_pages/core/offline_page_model.h"
#include "components/offline_pages/core/offline_page_test_archiver.h"
#include "components/offline_pages/core/offline_page_types.h"
#include "components/offline_pages/core/test_scoped_offline_clock.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "net/base/filename_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/test/test_timeouts.h"
#include "chrome/browser/download/android/mock_download_controller.h"
#include "components/gcm_driver/instance_id/instance_id_android.h"
#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h"
#endif
namespace offline_pages {
namespace {
const int64_t kTestFileSize = 876543LL;
const char* kTestPage1ClientId = "1234";
const char* kTestPage2ClientId = "5678";
const char* kTestPage3ClientId = "7890";
const char* kTestPage4ClientId = "42";
void RunTasksForDuration(base::TimeDelta delta) {
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), delta);
run_loop.Run();
}
} // namespace
class OfflinePageUtilsTest : public testing::Test,
public OfflinePageTestArchiver::Observer {
public:
OfflinePageUtilsTest();
~OfflinePageUtilsTest() override;
void SetUp() override;
void TearDown() override;
void SavePage(const GURL& url,
const ClientId& client_id,
std::unique_ptr<OfflinePageArchiver> archiver);
// Return number of matches found.
int FindRequestByNamespaceAndURL(const std::string& name_space,
const GURL& url);
size_t GetRequestCount() { return GetAllRequests().size(); }
// Wait until there are at least |min_request_count| requests.
void WaitForRequestMinCount(size_t min_request_count) {
for (;;) {
if (min_request_count <= GetRequestCount()) {
break;
}
RunTasksForDuration(base::Milliseconds(100));
}
}
RequestCoordinator* GetRequestCoordinator() {
return RequestCoordinatorFactory::GetForBrowserContext(profile());
}
OfflinePageUtils::DuplicateCheckResult CheckDuplicateDownloads(GURL url) {
OfflinePageUtils::DuplicateCheckResult result;
base::RunLoop run_loop;
auto quit = run_loop.QuitClosure();
auto on_done = [&](OfflinePageUtils::DuplicateCheckResult check_result) {
result = check_result;
quit.Run();
};
OfflinePageUtils::CheckDuplicateDownloads(
profile(), url, base::BindLambdaForTesting(on_done));
run_loop.Run();
return result;
}
std::optional<int64_t> GetCachedOfflinePageSizeBetween(
const base::Time& begin_time,
const base::Time& end_time) {
int64_t result;
base::RunLoop run_loop;
auto quit = run_loop.QuitClosure();
auto on_done = [&](int64_t size) {
result = size;
quit.Run();
};
if (!OfflinePageUtils::GetCachedOfflinePageSizeBetween(
profile(), base::BindLambdaForTesting(on_done), begin_time,
end_time)) {
return std::nullopt;
}
run_loop.Run();
return result;
}
// OfflinePageTestArchiver::Observer implementation:
void SetLastPathCreatedByArchiver(const base::FilePath& file_path) override {}
TestScopedOfflineClock* clock() { return &clock_; }
TestingProfile* profile() { return &profile_; }
content::WebContents* web_contents() const { return web_contents_.get(); }
void CreateCachedOfflinePages();
std::vector<std::unique_ptr<SavePageRequest>> GetAllRequests() {
base::RunLoop run_loop;
auto quit = run_loop.QuitClosure();
std::vector<std::unique_ptr<SavePageRequest>> result;
auto on_done = [&](std::vector<std::unique_ptr<SavePageRequest>> requests) {
result = std::move(requests);
quit.Run();
};
GetRequestCoordinator()->GetAllRequests(
base::BindLambdaForTesting(on_done));
run_loop.Run();
return result;
}
protected:
const GURL kTestPage1Url{"http://test.org/page1"};
const GURL kTestPage2Url{"http://test.org/page2"};
const GURL kTestPage3Url{"http://test.org/page3"};
const GURL kTestPage4Url{"http://test.org/page4"};
private:
void CreateOfflinePages();
void CreateRequests();
std::unique_ptr<OfflinePageTestArchiver> BuildArchiver(
const GURL& url,
const base::FilePath& file_name);
TestScopedOfflineClock clock_;
content::BrowserTaskEnvironment task_environment_;
TestingProfile profile_;
std::unique_ptr<content::WebContents> web_contents_;
base::test::ScopedFeatureList scoped_feature_list_;
#if BUILDFLAG(IS_ANDROID)
android::MockDownloadController download_controller_;
// OfflinePageTabHelper instantiates PrefetchService which in turn requests a
// fresh GCM token automatically. This causes the request to be done
// synchronously instead of with a posted task.
instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting
block_async_;
#endif
};
OfflinePageUtilsTest::OfflinePageUtilsTest() = default;
OfflinePageUtilsTest::~OfflinePageUtilsTest() {}
void OfflinePageUtilsTest::SetUp() {
// Create a test web contents.
web_contents_ = content::WebContents::Create(
content::WebContents::CreateParams(profile()));
OfflinePageTabHelper::CreateForWebContents(web_contents_.get());
// Reset the value of the test clock.
clock_.SetNow(base::Time::Now());
// Set up the factory for testing.
OfflinePageModelFactory::GetInstance()->SetTestingFactoryAndUse(
profile_.GetProfileKey(),
base::BindRepeating(&BuildTestOfflinePageModel));
RequestCoordinatorFactory::GetInstance()->SetTestingFactoryAndUse(
&profile_, base::BindRepeating(&BuildTestRequestCoordinator));
// Make sure to create offline pages and requests.
CreateOfflinePages();
// TODO(harringtond): I was surprised this test creates requests in Setup(),
// we should avoid this to be less surprising.
CreateRequests();
// This is needed in order to skip the logic to request storage permission.
#if BUILDFLAG(IS_ANDROID)
DownloadControllerBase::SetDownloadControllerBase(&download_controller_);
#endif
}
void OfflinePageUtilsTest::TearDown() {
#if BUILDFLAG(IS_ANDROID)
DownloadControllerBase::SetDownloadControllerBase(nullptr);
#endif
}
void OfflinePageUtilsTest::SavePage(
const GURL& url,
const ClientId& client_id,
std::unique_ptr<OfflinePageArchiver> archiver) {
OfflinePageModel::SavePageParams save_page_params;
save_page_params.url = url;
save_page_params.client_id = client_id;
base::RunLoop run_loop;
auto save_page_done = [&](SavePageResult result, int64_t offline_id) {
run_loop.QuitClosure().Run();
};
OfflinePageModelFactory::GetForBrowserContext(profile())->SavePage(
save_page_params, std::move(archiver), web_contents_.get(),
base::BindLambdaForTesting(save_page_done));
run_loop.Run();
}
void OfflinePageUtilsTest::CreateOfflinePages() {
// Create page 1.
std::unique_ptr<OfflinePageTestArchiver> archiver(BuildArchiver(
kTestPage1Url, base::FilePath(FILE_PATH_LITERAL("page1.mhtml"))));
offline_pages::ClientId client_id;
client_id.name_space = kDownloadNamespace;
client_id.id = kTestPage1ClientId;
SavePage(kTestPage1Url, client_id, std::move(archiver));
// Create page 2.
archiver = BuildArchiver(kTestPage2Url,
base::FilePath(FILE_PATH_LITERAL("page2.mhtml")));
client_id.id = kTestPage2ClientId;
SavePage(kTestPage2Url, client_id, std::move(archiver));
}
void OfflinePageUtilsTest::CreateRequests() {
RequestCoordinator::SavePageLaterParams params;
params.url = kTestPage3Url;
params.client_id =
offline_pages::ClientId(kDownloadNamespace, kTestPage3ClientId);
base::RunLoop run_loop;
auto quit = run_loop.QuitClosure();
auto page_saved = [&](AddRequestResult ignored) { quit.Run(); };
GetRequestCoordinator()->SavePageLater(
params, base::BindLambdaForTesting(page_saved));
run_loop.Run();
}
void OfflinePageUtilsTest::CreateCachedOfflinePages() {
// Add 4 temporary pages to the model used for test cases. And setting current
// time as the 00:00:00 time anchor.
offline_pages::ClientId client_id;
client_id.name_space = kBookmarkNamespace;
clock()->SetNow(base::Time::Now());
// Time 01:00:00.
clock()->Advance(base::Hours(1));
std::unique_ptr<OfflinePageTestArchiver> archiver(BuildArchiver(
kTestPage1Url, base::FilePath(FILE_PATH_LITERAL("page1.mhtml"))));
client_id.id = kTestPage1ClientId;
SavePage(kTestPage1Url, client_id, std::move(archiver));
// time 02:00:00.
clock()->Advance(base::Hours(1));
archiver = BuildArchiver(kTestPage2Url,
base::FilePath(FILE_PATH_LITERAL("page2.mhtml")));
client_id.id = kTestPage2ClientId;
SavePage(kTestPage2Url, client_id, std::move(archiver));
// time 03:00:00.
clock()->Advance(base::Hours(1));
archiver = BuildArchiver(kTestPage3Url,
base::FilePath(FILE_PATH_LITERAL("page3.mhtml")));
client_id.id = kTestPage3ClientId;
SavePage(kTestPage3Url, client_id, std::move(archiver));
// Add a temporary page to test boundary at 10:00:00.
clock()->Advance(base::Hours(7));
archiver = BuildArchiver(kTestPage4Url,
base::FilePath(FILE_PATH_LITERAL("page4.mhtml")));
client_id.id = kTestPage4ClientId;
SavePage(kTestPage4Url, client_id, std::move(archiver));
// Reset clock->to 03:00:00.
clock()->Advance(base::Hours(-7));
}
std::unique_ptr<OfflinePageTestArchiver> OfflinePageUtilsTest::BuildArchiver(
const GURL& url,
const base::FilePath& file_name) {
std::unique_ptr<OfflinePageTestArchiver> archiver(new OfflinePageTestArchiver(
this, url, OfflinePageArchiver::ArchiverResult::SUCCESSFULLY_CREATED,
std::u16string(), kTestFileSize, std::string(),
base::SingleThreadTaskRunner::GetCurrentDefault()));
archiver->set_filename(file_name);
return archiver;
}
int OfflinePageUtilsTest::FindRequestByNamespaceAndURL(
const std::string& name_space,
const GURL& url) {
std::vector<std::unique_ptr<SavePageRequest>> requests = GetAllRequests();
int matches = 0;
for (auto& request : requests) {
if (request->url() == url &&
request->client_id().name_space == name_space) {
matches++;
}
}
return matches;
}
TEST_F(OfflinePageUtilsTest, CheckDuplicateDownloads) {
// The duplicate page should be found for this.
EXPECT_EQ(OfflinePageUtils::DuplicateCheckResult::DUPLICATE_PAGE_FOUND,
CheckDuplicateDownloads(kTestPage1Url));
// The duplicate request should be found for this.
EXPECT_EQ(OfflinePageUtils::DuplicateCheckResult::DUPLICATE_REQUEST_FOUND,
CheckDuplicateDownloads(kTestPage3Url));
// No duplicate should be found for this.
EXPECT_EQ(OfflinePageUtils::DuplicateCheckResult::NOT_FOUND,
CheckDuplicateDownloads(kTestPage4Url));
}
TEST_F(OfflinePageUtilsTest, ScheduleDownload) {
// Pre-check.
ASSERT_EQ(0, FindRequestByNamespaceAndURL(kDownloadNamespace, kTestPage1Url));
ASSERT_EQ(1, FindRequestByNamespaceAndURL(kDownloadNamespace, kTestPage3Url));
ASSERT_EQ(0, FindRequestByNamespaceAndURL(kDownloadNamespace, kTestPage4Url));
// TODO(harringtond): Remove request creation in Setup().
size_t request_count_wait = 1;
// Re-downloading a page with duplicate page found.
OfflinePageUtils::ScheduleDownload(
web_contents(), kDownloadNamespace, kTestPage1Url,
OfflinePageUtils::DownloadUIActionFlags::NONE);
WaitForRequestMinCount(++request_count_wait);
EXPECT_EQ(1, FindRequestByNamespaceAndURL(kDownloadNamespace, kTestPage1Url));
// Re-downloading a page with duplicate request found.
OfflinePageUtils::ScheduleDownload(
web_contents(), kDownloadNamespace, kTestPage3Url,
OfflinePageUtils::DownloadUIActionFlags::NONE);
WaitForRequestMinCount(++request_count_wait);
EXPECT_EQ(2, FindRequestByNamespaceAndURL(kDownloadNamespace, kTestPage3Url));
// Downloading a page with no duplicate found.
OfflinePageUtils::ScheduleDownload(
web_contents(), kDownloadNamespace, kTestPage4Url,
OfflinePageUtils::DownloadUIActionFlags::NONE);
WaitForRequestMinCount(++request_count_wait);
EXPECT_EQ(1, FindRequestByNamespaceAndURL(kDownloadNamespace, kTestPage4Url));
}
#if BUILDFLAG(IS_ANDROID)
TEST_F(OfflinePageUtilsTest, ScheduleDownloadWithFailedFileAcecssRequest) {
DownloadControllerBase::Get()->SetApproveFileAccessRequestForTesting(false);
OfflinePageUtils::ScheduleDownload(
web_contents(), kDownloadNamespace, kTestPage4Url,
OfflinePageUtils::DownloadUIActionFlags::NONE);
// Here, we're waiting to make sure a request is not created. We can't use
// QuitClosure, since there's no callback threaded through ScheduleDownload.
// Instead, just wait a bit and assume ScheduleDownload is complete.
RunTasksForDuration(base::Seconds(1));
EXPECT_EQ(0, FindRequestByNamespaceAndURL(kDownloadNamespace, kTestPage4Url));
}
#endif
TEST_F(OfflinePageUtilsTest, TestGetCachedOfflinePageSizeBetween) {
// The clock will be at 03:00:00 after adding pages.
CreateCachedOfflinePages();
// Advance the clock so that we don't hit the time check boundary.
clock()->Advance(base::Minutes(5));
// Get the size of cached offline pages between 01:05:00 and 03:05:00.
EXPECT_EQ(kTestFileSize * 2,
GetCachedOfflinePageSizeBetween(clock()->Now() - base::Hours(2),
clock()->Now()));
}
TEST_F(OfflinePageUtilsTest, TestGetCachedOfflinePageSizeNoPageInModel) {
#if BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/40646823): Fix this test to run in < action_timeout()
// on the Android bots.
const base::test::ScopedRunLoopTimeout increased_run_timeout(
FROM_HERE, TestTimeouts::action_max_timeout());
#endif // BUILDFLAG(IS_ANDROID)
clock()->Advance(base::Hours(3));
// Get the size of cached offline pages between 01:00:00 and 03:00:00.
// Since no temporary pages were added to the model, the cache size should be
// 0.
EXPECT_EQ(0, GetCachedOfflinePageSizeBetween(clock()->Now() - base::Hours(2),
clock()->Now()));
}
TEST_F(OfflinePageUtilsTest, TestGetCachedOfflinePageSizeNoPageInRange) {
// The clock will be at 03:00:00 after adding pages.
CreateCachedOfflinePages();
// Advance the clock so that we don't hit the time check boundary.
clock()->Advance(base::Minutes(5));
// Get the size of cached offline pages between 03:04:00 and 03:05:00.
EXPECT_EQ(0, GetCachedOfflinePageSizeBetween(
clock()->Now() - base::Minutes(1), clock()->Now()));
}
TEST_F(OfflinePageUtilsTest, TestGetCachedOfflinePageSizeAllPagesInRange) {
// The clock will be at 03:00:00 after adding pages.
CreateCachedOfflinePages();
// Advance the clock to 23:00:00.
clock()->Advance(base::Hours(20));
// Get the size of cached offline pages between -01:00:00 and 23:00:00.
EXPECT_EQ(kTestFileSize * 4,
GetCachedOfflinePageSizeBetween(clock()->Now() - base::Hours(24),
clock()->Now()));
}
TEST_F(OfflinePageUtilsTest, TestGetCachedOfflinePageSizeAllPagesInvalidRange) {
// The clock will be at 03:00:00 after adding pages.
CreateCachedOfflinePages();
// Advance the clock to 23:00:00.
clock()->Advance(base::Hours(20));
// Get the size of cached offline pages between 23:00:00 and -01:00:00, which
// is an invalid range, the return value will be false and there will be no
// callback.
EXPECT_FALSE(GetCachedOfflinePageSizeBetween(
clock()->Now(), clock()->Now() - base::Hours(24)));
}
TEST_F(OfflinePageUtilsTest, TestGetCachedOfflinePageSizeEdgeCase) {
// The clock will be at 03:00:00 after adding pages.
CreateCachedOfflinePages();
// Get the size of cached offline pages between 02:00:00 and 03:00:00, since
// we are using a [begin_time, end_time) range so there will be only 1 page
// when query for this time range.
EXPECT_EQ(kTestFileSize * 1,
GetCachedOfflinePageSizeBetween(clock()->Now() - base::Hours(1),
clock()->Now()));
}
// Timeout on Android. http://crbug.com/981972
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_TestExtractOfflineHeaderValueFromNavigationEntry \
DISABLED_TestExtractOfflineHeaderValueFromNavigationEntry
#else
#define MAYBE_TestExtractOfflineHeaderValueFromNavigationEntry \
TestExtractOfflineHeaderValueFromNavigationEntry
#endif
TEST_F(OfflinePageUtilsTest,
MAYBE_TestExtractOfflineHeaderValueFromNavigationEntry) {
std::unique_ptr<content::NavigationEntry> entry(
content::NavigationEntry::Create());
std::string header_value;
// Expect empty string if no header is present.
header_value = OfflinePageUtils::ExtractOfflineHeaderValueFromNavigationEntry(
entry.get());
EXPECT_EQ("", header_value);
// Expect correct header value for correct header format.
entry->AddExtraHeaders("X-Chrome-offline: foo");
header_value = OfflinePageUtils::ExtractOfflineHeaderValueFromNavigationEntry(
entry.get());
EXPECT_EQ("foo", header_value);
// Expect empty string if multiple headers are set.
entry->AddExtraHeaders("Another-Header: bar");
header_value = OfflinePageUtils::ExtractOfflineHeaderValueFromNavigationEntry(
entry.get());
EXPECT_EQ("", header_value);
// Expect empty string for incorrect header format.
entry = content::NavigationEntry::Create();
entry->AddExtraHeaders("Random value");
header_value = OfflinePageUtils::ExtractOfflineHeaderValueFromNavigationEntry(
entry.get());
EXPECT_EQ("", header_value);
}
} // namespace offline_pages