// 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 "ash/ambient/managed/screensaver_image_downloader.h"
#include <memory>
#include <optional>
#include "ash/ambient/metrics/managed_screensaver_metrics.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/hash/sha1.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash {
namespace {
constexpr char kImageUrl1[] = "https://example.com/image1.jpg";
constexpr char kImageUrl2[] = "https://example.com/image2.jpg";
constexpr char kImageUrl3[] = "https://example.com/image3.jpg";
constexpr char kFileContents[] = "file contents";
constexpr char kCacheFileExt[] = ".cache";
constexpr char kTestDownloadFolder[] = "test_download_folder";
} // namespace
class ScreensaverImageDownloaderTest : public testing::Test {
public:
using ImageListUpdatedFuture =
base::test::TestFuture<const std::vector<base::FilePath>&>;
ScreensaverImageDownloaderTest() = default;
ScreensaverImageDownloaderTest(const ScreensaverImageDownloaderTest&) =
delete;
ScreensaverImageDownloaderTest& operator=(
const ScreensaverImageDownloaderTest&) = delete;
~ScreensaverImageDownloaderTest() override = default;
// testing::Test:
void SetUp() override {
EXPECT_TRUE(tmp_dir_.CreateUniqueTempDir());
test_download_folder_ = tmp_dir_.GetPath().AppendASCII(kTestDownloadFolder);
screensaver_image_downloader_ =
std::make_unique<ScreensaverImageDownloader>(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&url_loader_factory_),
test_download_folder_,
image_list_updated_future_.GetRepeatingCallback());
}
ScreensaverImageDownloader* screensaver_image_downloader() {
return screensaver_image_downloader_.get();
}
network::TestURLLoaderFactory* url_loader_factory() {
return &url_loader_factory_;
}
const base::FilePath& test_download_folder() { return test_download_folder_; }
base::test::TaskEnvironment* task_environment() { return &task_environment_; }
void DeleteTestDownloadFolder() {
EXPECT_TRUE(base::DeletePathRecursively(test_download_folder_));
}
void VerifyDownloadingQueueSize(size_t expected_size) const {
EXPECT_EQ(expected_size,
screensaver_image_downloader_->downloading_queue_.size());
}
void QueueNewImageDownload(const std::string& image_url) {
screensaver_image_downloader_->QueueImageDownload(image_url);
}
base::FilePath GetExpectedFilePath(const std::string url) {
auto hash = base::SHA1Hash(base::as_byte_span(url));
const std::string encoded_hash = base::HexEncode(hash);
return test_download_folder_.AppendASCII(encoded_hash + kCacheFileExt);
}
void VerifySucessfulImageRequest(
const std::vector<std::pair<base::FilePath, std::string>>&
expected_images) {
ASSERT_TRUE(image_list_updated_future_.Wait())
<< "Callback expected to be called.";
const std::vector<base::FilePath> image_list =
image_list_updated_future_.Take();
ASSERT_EQ(expected_images.size(), image_list.size());
for (const auto& [path, file_content] : expected_images) {
ASSERT_TRUE(base::Contains(image_list, path));
ASSERT_TRUE(base::PathExists(path));
std::string actual_file_contents;
EXPECT_TRUE(base::ReadFileToString(path, &actual_file_contents));
EXPECT_EQ(file_content, actual_file_contents);
}
}
void VerifyScreensaverImagesCacheSize(size_t expected_size) const {
EXPECT_EQ(expected_size,
screensaver_image_downloader_->GetScreensaverImages().size());
}
private:
base::test::TaskEnvironment task_environment_;
base::ScopedTempDir tmp_dir_;
base::FilePath test_download_folder_;
network::TestURLLoaderFactory url_loader_factory_;
ImageListUpdatedFuture image_list_updated_future_;
// Class under test
std::unique_ptr<ScreensaverImageDownloader> screensaver_image_downloader_;
};
TEST_F(ScreensaverImageDownloaderTest, DownloadImagesTest) {
base::HistogramTester histogram_tester;
// Setup the fake URL responses:
// * kImageUrl1 returns a valid response.
// * kImageUrl2 returns a 404 error.
// * kImageUrl3 deletes the download dir before returning a valid response.
url_loader_factory()->SetInterceptor(
base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
ASSERT_TRUE(request.url.is_valid());
if (request.url == kImageUrl1) {
url_loader_factory()->AddResponse(kImageUrl1, kFileContents);
}
if (request.url == kImageUrl2) {
auto response_head = network::mojom::URLResponseHead::New();
response_head->headers =
base::MakeRefCounted<net::HttpResponseHeaders>("");
response_head->headers->SetHeader("Content-Type", "image/jpg");
response_head->headers->ReplaceStatusLine("HTTP/1.1 404 Not found");
url_loader_factory()->AddResponse(
GURL(kImageUrl2), std::move(response_head), std::string(),
network::URLLoaderCompletionStatus(net::OK));
}
if (request.url == kImageUrl3) {
DeleteTestDownloadFolder();
url_loader_factory()->AddResponse(kImageUrl3, kFileContents);
}
}));
// Test successful download.
std::vector<std::pair<base::FilePath, std::string>> expected_images;
expected_images.emplace_back(GetExpectedFilePath(kImageUrl1),
std::string(kFileContents));
QueueNewImageDownload(kImageUrl1);
VerifySucessfulImageRequest(expected_images);
// Queue the request that should not download any file.
QueueNewImageDownload(kImageUrl2);
QueueNewImageDownload(kImageUrl3);
// Verify that the downloader did not create image files for the error
// downloads.
task_environment()->RunUntilIdle();
EXPECT_FALSE(base::PathExists(GetExpectedFilePath(kImageUrl2)));
EXPECT_FALSE(base::PathExists(GetExpectedFilePath(kImageUrl3)));
const std::string& histogram_name =
GetManagedScreensaverHistogram(kManagedScreensaverImageDownloadResultUMA);
histogram_tester.ExpectTotalCount(histogram_name, /*expected_count=*/3);
histogram_tester.ExpectBucketCount(
histogram_name,
/*sample=*/ScreensaverImageDownloadResult::kSuccess,
/*expected_count=*/1);
histogram_tester.ExpectBucketCount(
histogram_name,
/*sample=*/ScreensaverImageDownloadResult::kNetworkError,
/*expected_count=*/1);
histogram_tester.ExpectBucketCount(
histogram_name,
/*sample=*/ScreensaverImageDownloadResult::kFileSaveError,
/*expected_count=*/1);
}
TEST_F(ScreensaverImageDownloaderTest, ReuseFilesInCacheTest) {
// Track how many URL requests will be sent by the downloader
size_t urls_requested = 0;
url_loader_factory()->SetInterceptor(
base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
++urls_requested;
url_loader_factory()->AddResponse(kImageUrl1, kFileContents);
}));
// Test initial download.
std::vector<std::pair<base::FilePath, std::string>> expected_images;
expected_images.emplace_back(GetExpectedFilePath(kImageUrl1),
std::string(kFileContents));
QueueNewImageDownload(kImageUrl1);
VerifySucessfulImageRequest(expected_images);
EXPECT_EQ(1u, urls_requested);
// Attempting to download the same URL should not create a new network
// request.
QueueNewImageDownload(kImageUrl1);
VerifySucessfulImageRequest(expected_images);
EXPECT_EQ(1u, urls_requested);
url_loader_factory()->SetInterceptor(
base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
++urls_requested;
url_loader_factory()->AddResponse(kImageUrl2, kFileContents);
}));
// A different URL should create a new network request.
expected_images.emplace_back(GetExpectedFilePath(kImageUrl2),
std::string(kFileContents));
QueueNewImageDownload(kImageUrl2);
VerifySucessfulImageRequest(expected_images);
EXPECT_EQ(2u, urls_requested);
}
TEST_F(ScreensaverImageDownloaderTest, VerifySerializedDownloadTest) {
// Push two downloads and check the internal downloading queue
QueueNewImageDownload(kImageUrl1);
QueueNewImageDownload(kImageUrl2);
// First download should be executing and expecting the URL response, verify
// that the second download is in the queue
task_environment()->RunUntilIdle();
VerifyDownloadingQueueSize(1u);
// Resolve the first download
url_loader_factory()->AddResponse(kImageUrl1, kFileContents);
std::vector<std::pair<base::FilePath, std::string>> expected_images;
expected_images.emplace_back(GetExpectedFilePath(kImageUrl1),
std::string(kFileContents));
VerifySucessfulImageRequest(expected_images);
// First download has been resolved, second download should be executing and
// expecting the URL response.
task_environment()->RunUntilIdle();
VerifyDownloadingQueueSize(0u);
// Queue a third download while the second download is still waiting
QueueNewImageDownload(kImageUrl3);
task_environment()->RunUntilIdle();
VerifyDownloadingQueueSize(1u);
// Resolve the second download
url_loader_factory()->AddResponse(kImageUrl2, kFileContents);
expected_images.emplace_back(GetExpectedFilePath(kImageUrl2),
std::string(kFileContents));
VerifySucessfulImageRequest(expected_images);
task_environment()->RunUntilIdle();
VerifyDownloadingQueueSize(0u);
// Resolve the third download
url_loader_factory()->AddResponse(kImageUrl3, kFileContents);
expected_images.emplace_back(GetExpectedFilePath(kImageUrl3),
std::string(kFileContents));
VerifySucessfulImageRequest(expected_images);
// Ensure that the queue remains empty
task_environment()->RunUntilIdle();
VerifyDownloadingQueueSize(0u);
}
TEST_F(ScreensaverImageDownloaderTest,
DeleteDownloadedImagesWhenEmptyListIsPassedTest) {
// Download two images to attempt clearing later.
url_loader_factory()->AddResponse(kImageUrl1, kFileContents);
url_loader_factory()->AddResponse(kImageUrl2, kFileContents);
std::vector<std::pair<base::FilePath, std::string>> expected_images;
expected_images.emplace_back(GetExpectedFilePath(kImageUrl1),
std::string(kFileContents));
QueueNewImageDownload(kImageUrl1);
VerifySucessfulImageRequest(expected_images);
expected_images.emplace_back(GetExpectedFilePath(kImageUrl2),
std::string(kFileContents));
QueueNewImageDownload(kImageUrl2);
VerifySucessfulImageRequest(expected_images);
// Verify that images saved into disk are deleted properly.
screensaver_image_downloader()->UpdateImageUrlList(base::Value::List());
task_environment()->RunUntilIdle();
EXPECT_FALSE(base::PathExists(test_download_folder()));
VerifyScreensaverImagesCacheSize(0u);
}
TEST_F(ScreensaverImageDownloaderTest,
ClearRequestQueueWhenEmptyListIsPassedTest) {
base::HistogramTester histogram_tester;
// Queue 3 download request, the first one one will be waiting for the URL
// response, the latter will be queued.
QueueNewImageDownload(kImageUrl1);
QueueNewImageDownload(kImageUrl2);
QueueNewImageDownload(kImageUrl3);
task_environment()->RunUntilIdle();
VerifyDownloadingQueueSize(2u);
// Simulate a new policy update that clears the queue.
screensaver_image_downloader()->UpdateImageUrlList(base::Value::List());
// Resolve the request for the first image, the image should not be saved to
// file.
url_loader_factory()->AddResponse(kImageUrl1, kFileContents);
// Verify that the downloader did not create image files for the cancelled
// downloads.
task_environment()->RunUntilIdle();
EXPECT_FALSE(base::PathExists(GetExpectedFilePath(kImageUrl1)));
EXPECT_FALSE(base::PathExists(GetExpectedFilePath(kImageUrl2)));
EXPECT_FALSE(base::PathExists(GetExpectedFilePath(kImageUrl3)));
VerifyScreensaverImagesCacheSize(0u);
const std::string& histogram_name =
GetManagedScreensaverHistogram(kManagedScreensaverImageDownloadResultUMA);
histogram_tester.ExpectTotalCount(histogram_name, /*expected_count=*/2);
histogram_tester.ExpectBucketCount(
histogram_name,
/*sample=*/ScreensaverImageDownloadResult::kCancelled,
/*expected_count=*/2);
}
TEST_F(ScreensaverImageDownloaderTest, ClearImagesAfterUpdateTest) {
{
// Add two image to the policy list and confirm that are indeed downloaded.
base::Value::List image_urls;
image_urls.Append(kImageUrl1);
screensaver_image_downloader()->UpdateImageUrlList(image_urls);
url_loader_factory()->AddResponse(kImageUrl1, kFileContents);
std::vector<std::pair<base::FilePath, std::string>> expected_images;
expected_images.emplace_back(GetExpectedFilePath(kImageUrl1),
std::string(kFileContents));
VerifySucessfulImageRequest(expected_images);
image_urls.Append(kImageUrl2);
screensaver_image_downloader()->UpdateImageUrlList(image_urls);
url_loader_factory()->AddResponse(kImageUrl2, kFileContents);
VerifySucessfulImageRequest(expected_images);
expected_images.emplace_back(GetExpectedFilePath(kImageUrl2),
std::string(kFileContents));
VerifySucessfulImageRequest(expected_images);
}
{
// Case: Verify that when the first file is removed from policy image list
// and only the second file remains, the first file is indeed cleaned-up
// from the disk and the second file is still present on the disk.
base::Value::List image_urls;
image_urls.Append(kImageUrl2);
screensaver_image_downloader()->UpdateImageUrlList(image_urls);
// Verify the update callback from clearing the first image.
std::vector<std::pair<base::FilePath, std::string>> expected_images;
expected_images.emplace_back(GetExpectedFilePath(kImageUrl2),
std::string(kFileContents));
VerifySucessfulImageRequest(expected_images);
// Expect another callback from the second image found in cache.
VerifySucessfulImageRequest(expected_images);
// Verify files in disk.
task_environment()->RunUntilIdle();
EXPECT_FALSE(base::PathExists(GetExpectedFilePath(kImageUrl1)));
EXPECT_TRUE(base::PathExists(GetExpectedFilePath(kImageUrl2)));
VerifyScreensaverImagesCacheSize(1u);
}
{
// Case: Verify that old unreferenced files are cleaned up from the disk.
// This can happen if the chromebook restarted, but the policy was updated
// while the chromebook was offline and it only receives the new policy
// values and is unaware of the old policy values.
std::string filename = "test";
base::FilePath orphan_cache_file =
test_download_folder().AppendASCII(filename + kCacheFileExt);
base::WriteFile(orphan_cache_file, "test_data");
EXPECT_TRUE(base::PathExists(orphan_cache_file));
base::Value::List image_urls;
image_urls.Append(kImageUrl2);
screensaver_image_downloader()->UpdateImageUrlList(image_urls);
VerifySucessfulImageRequest(
{{GetExpectedFilePath(kImageUrl2), std::string(kFileContents)}});
task_environment()->RunUntilIdle();
EXPECT_TRUE(base::PathExists(GetExpectedFilePath(kImageUrl2)));
// Confirm that after the update the orphan file was successfully cleaned
// up.
EXPECT_FALSE(base::PathExists(orphan_cache_file));
VerifyScreensaverImagesCacheSize(1u);
}
}
} // namespace ash