blob: d22c695ed29752c9561d616a7fbcc0b5cae92b53 [file] [log] [blame]
// Copyright 2014 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 "components/search_provider_logos/logo_service_impl.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include <vector>
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/simple_test_clock.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/image_fetcher/core/image_decoder.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_data.h"
#include "components/search_engines/template_url_service.h"
#include "components/search_provider_logos/features.h"
#include "components/search_provider_logos/fixed_logo_api.h"
#include "components/search_provider_logos/google_logo_api.h"
#include "components/search_provider_logos/logo_cache.h"
#include "components/search_provider_logos/logo_observer.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_request_test_util.h"
#include "services/identity/public/cpp/identity_test_environment.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image.h"
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AtMost;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::Mock;
using ::testing::Ne;
using ::testing::NiceMock;
using ::testing::Not;
using ::testing::NotNull;
using ::testing::Pointee;
using ::testing::Return;
using ::testing::StrictMock;
using sync_preferences::TestingPrefServiceSyncable;
namespace search_provider_logos {
namespace {
using MockLogoCallback = base::MockCallback<LogoCallback>;
using MockEncodedLogoCallback = base::MockCallback<EncodedLogoCallback>;
scoped_refptr<base::RefCountedString> EncodeBitmapAsPNG(
const SkBitmap& bitmap) {
scoped_refptr<base::RefCountedMemory> png_bytes =
gfx::Image::CreateFrom1xBitmap(bitmap).As1xPNGBytes();
scoped_refptr<base::RefCountedString> str = new base::RefCountedString();
str->data().assign(png_bytes->front_as<char>(), png_bytes->size());
return str;
}
std::string EncodeBitmapAsPNGBase64(const SkBitmap& bitmap) {
scoped_refptr<base::RefCountedString> png_bytes = EncodeBitmapAsPNG(bitmap);
std::string encoded_image_base64;
base::Base64Encode(png_bytes->data(), &encoded_image_base64);
return encoded_image_base64;
}
SkBitmap MakeBitmap(int width, int height) {
SkBitmap bitmap;
bitmap.allocN32Pixels(width, height);
bitmap.eraseColor(SK_ColorBLUE);
return bitmap;
}
EncodedLogo EncodeLogo(const Logo& logo) {
EncodedLogo encoded_logo;
encoded_logo.encoded_image = EncodeBitmapAsPNG(logo.image);
encoded_logo.metadata = logo.metadata;
return encoded_logo;
}
Logo DecodeLogo(const EncodedLogo& encoded_logo) {
Logo logo;
logo.image =
gfx::Image::CreateFrom1xPNGBytes(encoded_logo.encoded_image->front(),
encoded_logo.encoded_image->size())
.AsBitmap();
logo.metadata = encoded_logo.metadata;
return logo;
}
Logo GetSampleLogo(const GURL& logo_url, base::Time response_time) {
Logo logo;
logo.image = MakeBitmap(2, 5);
logo.metadata.can_show_after_expiration = false;
logo.metadata.expiration_time =
response_time + base::TimeDelta::FromHours(19);
logo.metadata.fingerprint = "8bc33a80";
logo.metadata.source_url =
AppendPreliminaryParamsToDoodleURL(false, logo_url);
logo.metadata.on_click_url = GURL("https://www.google.com/search?q=potato");
logo.metadata.alt_text = "A logo about potatoes";
logo.metadata.animated_url = GURL("https://www.google.com/logos/doodle.png");
logo.metadata.mime_type = "image/png";
return logo;
}
Logo GetSampleLogo2(const GURL& logo_url, base::Time response_time) {
Logo logo;
logo.image = MakeBitmap(4, 3);
logo.metadata.can_show_after_expiration = true;
logo.metadata.expiration_time = base::Time();
logo.metadata.fingerprint = "71082741021409127";
logo.metadata.source_url =
AppendPreliminaryParamsToDoodleURL(false, logo_url);
logo.metadata.on_click_url = GURL("https://example.com/page25");
logo.metadata.alt_text = "The logo for example.com";
logo.metadata.mime_type = "image/jpeg";
return logo;
}
std::string MakeServerResponse(const SkBitmap& image,
const std::string& on_click_url,
const std::string& alt_text,
const std::string& animated_url,
const std::string& mime_type,
const std::string& fingerprint,
base::TimeDelta time_to_live) {
base::DictionaryValue dict;
std::string data_uri = "data:";
data_uri += mime_type;
data_uri += ";base64,";
data_uri += EncodeBitmapAsPNGBase64(image);
dict.SetString("ddljson.target_url", on_click_url);
dict.SetString("ddljson.alt_text", alt_text);
if (animated_url.empty()) {
dict.SetString("ddljson.doodle_type", "SIMPLE");
if (!image.isNull())
dict.SetString("ddljson.data_uri", data_uri);
} else {
dict.SetString("ddljson.doodle_type", "ANIMATED");
dict.SetBoolean("ddljson.large_image.is_animated_gif", true);
dict.SetString("ddljson.large_image.url", animated_url);
if (!image.isNull())
dict.SetString("ddljson.cta_data_uri", data_uri);
}
dict.SetString("ddljson.fingerprint", fingerprint);
if (time_to_live != base::TimeDelta())
dict.SetInteger("ddljson.time_to_live_ms",
static_cast<int>(time_to_live.InMilliseconds()));
std::string output;
base::JSONWriter::Write(dict, &output);
return output;
}
std::string MakeServerResponse(const Logo& logo, base::TimeDelta time_to_live) {
return MakeServerResponse(
logo.image, logo.metadata.on_click_url.spec(), logo.metadata.alt_text,
logo.metadata.animated_url.spec(), logo.metadata.mime_type,
logo.metadata.fingerprint, time_to_live);
}
template <typename Arg, typename Matcher>
bool Match(const Arg& arg,
const Matcher& matcher,
::testing::MatchResultListener* result_listener) {
return ::testing::Matcher<Arg>(matcher).MatchAndExplain(arg, result_listener);
}
MATCHER_P(DecodesTo, decoded_logo, "") {
return Match(DecodeLogo(arg), Eq(decoded_logo), result_listener);
}
class MockLogoCache : public LogoCache {
public:
MockLogoCache() : LogoCache(base::FilePath()) {
// Delegate actions to the *Internal() methods by default.
ON_CALL(*this, UpdateCachedLogoMetadata(_))
.WillByDefault(
Invoke(this, &MockLogoCache::UpdateCachedLogoMetadataInternal));
ON_CALL(*this, GetCachedLogoMetadata())
.WillByDefault(
Invoke(this, &MockLogoCache::GetCachedLogoMetadataInternal));
ON_CALL(*this, SetCachedLogo(_))
.WillByDefault(Invoke(this, &MockLogoCache::SetCachedLogoInternal));
}
MOCK_METHOD1(UpdateCachedLogoMetadata, void(const LogoMetadata& metadata));
MOCK_METHOD0(GetCachedLogoMetadata, const LogoMetadata*());
MOCK_METHOD1(SetCachedLogo, void(const EncodedLogo* logo));
// GetCachedLogo() can't be mocked since it returns a scoped_ptr, which is
// non-copyable. Instead create a method that's pinged when GetCachedLogo() is
// called.
MOCK_METHOD0(OnGetCachedLogo, void());
void EncodeAndSetCachedLogo(const Logo& logo) {
EncodedLogo encoded_logo = EncodeLogo(logo);
SetCachedLogo(&encoded_logo);
}
void ExpectSetCachedLogo(const Logo* expected_logo) {
Mock::VerifyAndClearExpectations(this);
if (expected_logo) {
EXPECT_CALL(*this, SetCachedLogo(Pointee(DecodesTo(*expected_logo))));
} else {
EXPECT_CALL(*this, SetCachedLogo(nullptr));
}
}
void UpdateCachedLogoMetadataInternal(const LogoMetadata& metadata) {
ASSERT_TRUE(logo_.get());
ASSERT_TRUE(metadata_.get());
EXPECT_EQ(metadata_->fingerprint, metadata.fingerprint);
metadata_.reset(new LogoMetadata(metadata));
logo_->metadata = metadata;
}
const LogoMetadata* GetCachedLogoMetadataInternal() {
return metadata_.get();
}
void SetCachedLogoInternal(const EncodedLogo* logo) {
logo_ = logo ? std::make_unique<EncodedLogo>(*logo) : nullptr;
metadata_ = logo ? std::make_unique<LogoMetadata>(logo->metadata) : nullptr;
}
std::unique_ptr<EncodedLogo> GetCachedLogo() override {
OnGetCachedLogo();
return logo_ ? std::make_unique<EncodedLogo>(*logo_) : nullptr;
}
private:
std::unique_ptr<LogoMetadata> metadata_;
std::unique_ptr<EncodedLogo> logo_;
};
class FakeImageDecoder : public image_fetcher::ImageDecoder {
public:
void DecodeImage(const std::string& image_data,
const gfx::Size& desired_image_frame_size,
image_fetcher::ImageDecodedCallback callback) override {
gfx::Image image = gfx::Image::CreateFrom1xPNGBytes(
reinterpret_cast<const uint8_t*>(image_data.data()), image_data.size());
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), image));
}
};
// A helper class that wraps around all the dependencies required to simulate
// signing in/out.
class SigninHelper {
public:
explicit SigninHelper() : identity_test_env_(&test_url_loader_factory_) {}
identity::IdentityManager* identity_manager() {
return identity_test_env_.identity_manager();
}
void SignIn() {
std::string email("user@gmail.com");
identity_test_env_.SetCookieAccounts(
{{email, identity::GetTestGaiaIdForEmail(email)}});
}
void SignOut() {
identity_test_env_.SetCookieAccounts({});
}
private:
network::TestURLLoaderFactory test_url_loader_factory_;
identity::IdentityTestEnvironment identity_test_env_;
};
class LogoServiceImplTest : public ::testing::Test {
protected:
LogoServiceImplTest()
: template_url_service_(nullptr, 0),
logo_cache_(new NiceMock<MockLogoCache>()),
shared_factory_(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_)),
use_gray_background_(false) {
test_url_loader_factory_.SetInterceptor(base::BindRepeating(
&LogoServiceImplTest::CapturingInterceptor, base::Unretained(this)));
// Default search engine with logo. All 3P doodle_urls use ddljson API.
AddSearchEngine("ex", "Logo Example",
"https://example.com/?q={searchTerms}",
GURL("https://example.com/logo.json"),
/*make_default=*/true);
test_clock_.SetNow(base::Time::FromJsTime(INT64_C(1388686828000)));
logo_service_ = std::make_unique<LogoServiceImpl>(
base::FilePath(), signin_helper_.identity_manager(),
&template_url_service_, std::make_unique<FakeImageDecoder>(),
shared_factory_,
base::BindRepeating(&LogoServiceImplTest::use_gray_background,
base::Unretained(this)));
logo_service_->SetClockForTests(&test_clock_);
logo_service_->SetLogoCacheForTests(base::WrapUnique(logo_cache_));
}
void TearDown() override {
// |logo_service_|'s owns |logo_cache_|, which gets destroyed on
// a background sequence after the LogoService's destruction. Ensure that
// |logo_cache_| is actually destroyed before the test ends to make gmock
// happy.
logo_service_->Shutdown();
logo_service_.reset();
task_environment_.RunUntilIdle();
}
// Returns the response that the server would send for the given logo.
std::string ServerResponse(const Logo& logo);
// Sets the response to be returned when the LogoService fetches the logo.
void SetServerResponse(const std::string& response,
int error_code = net::OK,
net::HttpStatusCode response_code = net::HTTP_OK);
// Sets the response to be returned when the LogoService fetches the logo and
// provides the given fingerprint.
void SetServerResponseWhenFingerprint(
const std::string& fingerprint,
const std::string& response_when_fingerprint,
int error_code = net::OK,
net::HttpStatusCode response_code = net::HTTP_OK);
const GURL& DoodleURL() const;
// Calls logo_service_->GetLogo() and waits for the asynchronous response(s).
void GetLogo(LogoCallbacks callbacks);
void GetDecodedLogo(LogoCallback cached, LogoCallback fresh);
void GetEncodedLogo(EncodedLogoCallback cached, EncodedLogoCallback fresh);
void AddSearchEngine(base::StringPiece keyword,
base::StringPiece short_name,
const std::string& url,
GURL doodle_url,
bool make_default);
void CapturingInterceptor(const network::ResourceRequest& request);
bool use_gray_background() const { return use_gray_background_; }
base::test::ScopedTaskEnvironment task_environment_;
TemplateURLService template_url_service_;
base::SimpleTestClock test_clock_;
NiceMock<MockLogoCache>* logo_cache_;
// Used for mocking |logo_service_| URLs.
network::TestURLLoaderFactory test_url_loader_factory_;
scoped_refptr<network::SharedURLLoaderFactory> shared_factory_;
std::unique_ptr<LogoServiceImpl> logo_service_;
SigninHelper signin_helper_;
GURL latest_url_;
bool use_gray_background_;
};
void LogoServiceImplTest::CapturingInterceptor(
const network::ResourceRequest& request) {
latest_url_ = request.url;
}
std::string LogoServiceImplTest::ServerResponse(const Logo& logo) {
base::TimeDelta time_to_live;
if (!logo.metadata.expiration_time.is_null())
time_to_live = logo.metadata.expiration_time - test_clock_.Now();
return MakeServerResponse(logo, time_to_live);
}
void LogoServiceImplTest::SetServerResponse(const std::string& response,
int error_code,
net::HttpStatusCode response_code) {
SetServerResponseWhenFingerprint(std::string(), response, error_code,
response_code);
}
void LogoServiceImplTest::SetServerResponseWhenFingerprint(
const std::string& fingerprint,
const std::string& response_when_fingerprint,
int error_code,
net::HttpStatusCode response_code) {
GURL url_with_fp = AppendFingerprintParamToDoodleURL(
AppendPreliminaryParamsToDoodleURL(false, DoodleURL()), fingerprint);
network::ResourceResponseHead head;
std::string headers(base::StringPrintf(
"HTTP/1.1 %d %s\nContent-type: text/html\n\n",
static_cast<int>(response_code), GetHttpReasonPhrase(response_code)));
head.headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(headers));
head.mime_type = "text/html";
network::URLLoaderCompletionStatus status;
status.error_code = error_code;
status.decoded_body_length = response_when_fingerprint.size();
test_url_loader_factory_.AddResponse(url_with_fp, head,
response_when_fingerprint, status);
}
const GURL& LogoServiceImplTest::DoodleURL() const {
return template_url_service_.GetDefaultSearchProvider()->doodle_url();
}
void LogoServiceImplTest::GetLogo(LogoCallbacks callbacks) {
logo_service_->GetLogo(std::move(callbacks));
task_environment_.RunUntilIdle();
}
void LogoServiceImplTest::GetDecodedLogo(LogoCallback cached,
LogoCallback fresh) {
LogoCallbacks callbacks;
callbacks.on_cached_decoded_logo_available = std::move(cached);
callbacks.on_fresh_decoded_logo_available = std::move(fresh);
GetLogo(std::move(callbacks));
}
void LogoServiceImplTest::GetEncodedLogo(EncodedLogoCallback cached,
EncodedLogoCallback fresh) {
LogoCallbacks callbacks;
callbacks.on_cached_encoded_logo_available = std::move(cached);
callbacks.on_fresh_encoded_logo_available = std::move(fresh);
GetLogo(std::move(callbacks));
}
void LogoServiceImplTest::AddSearchEngine(base::StringPiece keyword,
base::StringPiece short_name,
const std::string& url,
GURL doodle_url,
bool make_default) {
TemplateURLData search_url;
search_url.SetKeyword(base::ASCIIToUTF16(keyword));
search_url.SetShortName(base::ASCIIToUTF16(short_name));
search_url.SetURL(url);
search_url.doodle_url = doodle_url;
TemplateURL* template_url =
template_url_service_.Add(std::make_unique<TemplateURL>(search_url));
if (make_default) {
template_url_service_.SetUserSelectedDefaultSearchProvider(template_url);
}
}
// Tests -----------------------------------------------------------------------
TEST_F(LogoServiceImplTest, CTARequestedBackgroundCanUpdate) {
std::string response =
ServerResponse(GetSampleLogo(DoodleURL(), test_clock_.Now()));
GURL query_with_gray_background = AppendFingerprintParamToDoodleURL(
AppendPreliminaryParamsToDoodleURL(true, DoodleURL()), std::string());
GURL query_without_gray_background = AppendFingerprintParamToDoodleURL(
AppendPreliminaryParamsToDoodleURL(false, DoodleURL()), std::string());
use_gray_background_ = false;
test_url_loader_factory_.ClearResponses();
test_url_loader_factory_.AddResponse(query_without_gray_background.spec(),
response, net::HTTP_OK);
{
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(fresh, Run(_, _));
LogoCallbacks callbacks;
callbacks.on_fresh_decoded_logo_available = fresh.Get();
logo_service_->GetLogo(std::move(callbacks));
task_environment_.RunUntilIdle();
}
EXPECT_EQ(latest_url_.query().find("graybg:1"), std::string::npos);
use_gray_background_ = true;
test_url_loader_factory_.ClearResponses();
test_url_loader_factory_.AddResponse(query_with_gray_background.spec(),
response, net::HTTP_OK);
{
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(fresh, Run(_, _));
LogoCallbacks callbacks;
callbacks.on_fresh_decoded_logo_available = fresh.Get();
logo_service_->GetLogo(std::move(callbacks));
task_environment_.RunUntilIdle();
}
EXPECT_NE(latest_url_.query().find("graybg:1"), std::string::npos);
}
TEST_F(LogoServiceImplTest, DownloadAndCacheLogo) {
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
SetServerResponse(ServerResponse(logo));
logo_cache_->ExpectSetCachedLogo(&logo);
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, DownloadAndCacheEncodedLogo) {
StrictMock<MockEncodedLogoCallback> cached;
StrictMock<MockEncodedLogoCallback> fresh;
Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
EncodedLogo encoded_logo = EncodeLogo(logo);
SetServerResponse(ServerResponse(logo));
logo_cache_->ExpectSetCachedLogo(&logo);
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(encoded_logo)));
GetEncodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, ShouldReturnDisabledWhenDSEHasNoLogo) {
AddSearchEngine("cr", "Chromium", "https://www.chromium.org/?q={searchTerms}",
GURL(/* logo disabled */), /*make_default=*/true);
{
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
{
StrictMock<MockEncodedLogoCallback> cached;
StrictMock<MockEncodedLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DISABLED, Eq(base::nullopt)));
GetEncodedLogo(cached.Get(), fresh.Get());
}
}
TEST_F(LogoServiceImplTest, EmptyCacheAndFailedDownload) {
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());
{
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
SetServerResponse("server is borked");
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
{
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
{
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
SetServerResponse("", net::OK, net::HTTP_BAD_GATEWAY);
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
}
TEST_F(LogoServiceImplTest, AcceptMinimalLogoResponse) {
Logo logo;
logo.image = MakeBitmap(1, 2);
logo.metadata.source_url =
AppendPreliminaryParamsToDoodleURL(false, DoodleURL());
logo.metadata.can_show_after_expiration = true;
logo.metadata.mime_type = "image/png";
std::string response =
")]}' {\"ddljson\":{\"data_uri\":\"data:image/png;base64," +
EncodeBitmapAsPNGBase64(logo.image) + "\"}}";
SetServerResponse(response);
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, ReturnCachedLogo) {
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint, "",
net::ERR_FAILED, net::HTTP_OK);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, ValidateCachedLogo) {
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
// During revalidation, the image data and mime_type are absent.
Logo fresh_logo = cached_logo;
fresh_logo.image.reset();
fresh_logo.metadata.mime_type.clear();
fresh_logo.metadata.expiration_time =
test_clock_.Now() + base::TimeDelta::FromDays(8);
SetServerResponseWhenFingerprint(fresh_logo.metadata.fingerprint,
ServerResponse(fresh_logo));
{
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(1);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
ASSERT_TRUE(logo_cache_->GetCachedLogoMetadata());
EXPECT_EQ(fresh_logo.metadata.expiration_time,
logo_cache_->GetCachedLogoMetadata()->expiration_time);
{
// Ensure that cached logo is still returned correctly on subsequent
// requests. In particular, the metadata should stay valid.
// https://crbug.com/480090
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(1);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
}
TEST_F(LogoServiceImplTest, UpdateCachedLogoMetadata) {
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
Logo fresh_logo = cached_logo;
fresh_logo.image.reset();
fresh_logo.metadata.mime_type.clear();
fresh_logo.metadata.on_click_url = GURL("https://new.onclick.url");
fresh_logo.metadata.alt_text = "new alt text";
fresh_logo.metadata.animated_url = GURL("https://new.animated.url");
fresh_logo.metadata.expiration_time =
test_clock_.Now() + base::TimeDelta::FromDays(8);
SetServerResponseWhenFingerprint(fresh_logo.metadata.fingerprint,
ServerResponse(fresh_logo));
// On the first request, the cached logo should be used.
{
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
// Subsequently, the cached image should be returned along with the updated
// metadata.
{
Logo expected_logo = fresh_logo;
expected_logo.image = cached_logo.image;
expected_logo.metadata.mime_type = cached_logo.metadata.mime_type;
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(expected_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::REVALIDATED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
}
TEST_F(LogoServiceImplTest, UpdateCachedLogo) {
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
Logo fresh_logo = GetSampleLogo2(DoodleURL(), test_clock_.Now());
SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint,
ServerResponse(fresh_logo));
logo_cache_->ExpectSetCachedLogo(&fresh_logo);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(fresh_logo)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, InvalidateCachedLogo) {
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
// This response means there's no current logo.
SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint,
")]}' {\"update\":{}}");
logo_cache_->ExpectSetCachedLogo(nullptr);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, DeleteCachedLogoFromOldUrl) {
SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
Logo cached_logo =
GetSampleLogo(GURL("https://oldsearchprovider.com"), test_clock_.Now());
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, LogoWithTTLCannotBeShownAfterExpiration) {
Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
base::TimeDelta time_to_live = base::TimeDelta::FromDays(3);
logo.metadata.expiration_time = test_clock_.Now() + time_to_live;
SetServerResponse(ServerResponse(logo));
LogoCallbacks callbacks;
callbacks.on_fresh_decoded_logo_available = base::Bind(
[](LogoCallbackReason type, const base::Optional<Logo>& logo) {});
GetLogo(std::move(callbacks));
const LogoMetadata* cached_metadata = logo_cache_->GetCachedLogoMetadata();
ASSERT_TRUE(cached_metadata);
EXPECT_FALSE(cached_metadata->can_show_after_expiration);
EXPECT_EQ(test_clock_.Now() + time_to_live, cached_metadata->expiration_time);
}
TEST_F(LogoServiceImplTest, LogoWithoutTTLCanBeShownAfterExpiration) {
Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
base::TimeDelta time_to_live = base::TimeDelta();
SetServerResponse(MakeServerResponse(logo, time_to_live));
LogoCallbacks callbacks;
callbacks.on_fresh_decoded_logo_available = base::Bind(
[](LogoCallbackReason type, const base::Optional<Logo>& logo) {});
GetLogo(std::move(callbacks));
const LogoMetadata* cached_metadata = logo_cache_->GetCachedLogoMetadata();
ASSERT_TRUE(cached_metadata);
EXPECT_TRUE(cached_metadata->can_show_after_expiration);
EXPECT_EQ(test_clock_.Now() + base::TimeDelta::FromDays(30),
cached_metadata->expiration_time);
}
TEST_F(LogoServiceImplTest, UseSoftExpiredCachedLogo) {
SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
cached_logo.metadata.expiration_time =
test_clock_.Now() - base::TimeDelta::FromSeconds(1);
cached_logo.metadata.can_show_after_expiration = true;
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, RerequestSoftExpiredCachedLogo) {
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
cached_logo.metadata.expiration_time =
test_clock_.Now() - base::TimeDelta::FromDays(5);
cached_logo.metadata.can_show_after_expiration = true;
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
Logo fresh_logo = GetSampleLogo2(DoodleURL(), test_clock_.Now());
SetServerResponse(ServerResponse(fresh_logo));
logo_cache_->ExpectSetCachedLogo(&fresh_logo);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(fresh_logo)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, DeleteAncientCachedLogo) {
SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
cached_logo.metadata.expiration_time =
test_clock_.Now() - base::TimeDelta::FromDays(200);
cached_logo.metadata.can_show_after_expiration = true;
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, DeleteExpiredCachedLogo) {
SetServerResponse("", net::ERR_FAILED, net::HTTP_OK);
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
cached_logo.metadata.expiration_time =
test_clock_.Now() - base::TimeDelta::FromSeconds(1);
cached_logo.metadata.can_show_after_expiration = false;
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
EXPECT_CALL(*logo_cache_, UpdateCachedLogoMetadata(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(0);
EXPECT_CALL(*logo_cache_, SetCachedLogo(nullptr)).Times(AnyNumber());
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(1));
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::FAILED, Eq(base::nullopt)));
GetDecodedLogo(cached.Get(), fresh.Get());
}
TEST_F(LogoServiceImplTest, ClearLogoOnSignOut) {
// Sign in and setup a logo response.
signin_helper_.SignIn();
Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
SetServerResponse(ServerResponse(logo));
// Request the logo so it gets fetched and cached.
logo_cache_->ExpectSetCachedLogo(&logo);
StrictMock<MockLogoCallback> cached;
StrictMock<MockLogoCallback> fresh;
EXPECT_CALL(cached, Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
GetDecodedLogo(cached.Get(), fresh.Get());
// Signing out should clear the cached logo immediately.
logo_cache_->ExpectSetCachedLogo(nullptr);
signin_helper_.SignOut();
}
// Tests that deal with multiple listeners.
void EnqueueCallbacks(LogoServiceImpl* logo_service,
std::vector<LogoCallback>* cached_callbacks,
std::vector<LogoCallback>* fresh_callbacks,
size_t start_index) {
DCHECK_EQ(cached_callbacks->size(), fresh_callbacks->size());
if (start_index >= cached_callbacks->size())
return;
LogoCallbacks callbacks;
callbacks.on_cached_decoded_logo_available =
std::move((*cached_callbacks)[start_index]);
callbacks.on_fresh_decoded_logo_available =
std::move((*fresh_callbacks)[start_index]);
logo_service->GetLogo(std::move(callbacks));
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&EnqueueCallbacks, logo_service, cached_callbacks,
fresh_callbacks, start_index + 1));
}
TEST_F(LogoServiceImplTest, SupportOverlappingLogoRequests) {
Logo cached_logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
logo_cache_->EncodeAndSetCachedLogo(cached_logo);
ON_CALL(*logo_cache_, SetCachedLogo(_)).WillByDefault(Return());
Logo fresh_logo = GetSampleLogo2(DoodleURL(), test_clock_.Now());
std::string response = ServerResponse(fresh_logo);
SetServerResponse(response);
SetServerResponseWhenFingerprint(cached_logo.metadata.fingerprint, response);
const int kNumListeners = 10;
std::vector<std::unique_ptr<MockLogoCallback>> mocks;
std::vector<LogoCallback> cached_callbacks;
std::vector<LogoCallback> fresh_callbacks;
for (int i = 0; i < kNumListeners; ++i) {
mocks.push_back(std::make_unique<MockLogoCallback>());
EXPECT_CALL(*mocks.back(),
Run(LogoCallbackReason::DETERMINED, Eq(cached_logo)));
cached_callbacks.push_back(mocks.back()->Get());
mocks.push_back(std::make_unique<MockLogoCallback>());
EXPECT_CALL(*mocks.back(),
Run(LogoCallbackReason::DETERMINED, Eq(fresh_logo)));
fresh_callbacks.push_back(mocks.back()->Get());
}
EXPECT_CALL(*logo_cache_, SetCachedLogo(_)).Times(AtMost(3));
EXPECT_CALL(*logo_cache_, OnGetCachedLogo()).Times(AtMost(3));
EnqueueCallbacks(logo_service_.get(), &cached_callbacks, &fresh_callbacks, 0);
task_environment_.RunUntilIdle();
}
TEST_F(LogoServiceImplTest, DeleteCallbacksWhenLogoURLChanged) {
StrictMock<MockLogoCallback> first_cached;
StrictMock<MockLogoCallback> first_fresh;
EXPECT_CALL(first_cached,
Run(LogoCallbackReason::CANCELED, Eq(base::nullopt)));
EXPECT_CALL(first_fresh,
Run(LogoCallbackReason::CANCELED, Eq(base::nullopt)));
LogoCallbacks first_callbacks;
first_callbacks.on_cached_decoded_logo_available = first_cached.Get();
first_callbacks.on_fresh_decoded_logo_available = first_fresh.Get();
logo_service_->GetLogo(std::move(first_callbacks));
// Change default search engine; new DSE has a doodle URL.
AddSearchEngine("cr", "Chromium", "https://www.chromium.org/?q={searchTerms}",
GURL("https://chromium.org/logo.json"),
/*make_default=*/true);
Logo logo = GetSampleLogo(DoodleURL(), test_clock_.Now());
SetServerResponse(ServerResponse(logo));
StrictMock<MockLogoCallback> second_cached;
StrictMock<MockLogoCallback> second_fresh;
EXPECT_CALL(second_cached,
Run(LogoCallbackReason::DETERMINED, Eq(base::nullopt)));
EXPECT_CALL(second_fresh, Run(LogoCallbackReason::DETERMINED, Eq(logo)));
LogoCallbacks second_callbacks;
second_callbacks.on_cached_decoded_logo_available = second_cached.Get();
second_callbacks.on_fresh_decoded_logo_available = second_fresh.Get();
logo_service_->GetLogo(std::move(second_callbacks));
task_environment_.RunUntilIdle();
}
} // namespace
bool operator==(const Logo& a, const Logo& b) {
return (a.image.width() == b.image.width()) &&
(a.image.height() == b.image.height()) &&
(a.metadata.on_click_url == b.metadata.on_click_url) &&
(a.metadata.source_url == b.metadata.source_url) &&
(a.metadata.animated_url == b.metadata.animated_url) &&
(a.metadata.alt_text == b.metadata.alt_text) &&
(a.metadata.mime_type == b.metadata.mime_type) &&
(a.metadata.fingerprint == b.metadata.fingerprint) &&
(a.metadata.can_show_after_expiration ==
b.metadata.can_show_after_expiration);
}
bool operator==(const EncodedLogo& a, const EncodedLogo& b) {
return DecodeLogo(a) == DecodeLogo(b);
}
void PrintTo(const Logo& logo, std::ostream* ostr) {
*ostr << "image size: " << logo.image.width() << "x" << logo.image.height()
<< "\non_click_url: " << logo.metadata.on_click_url
<< "\nsource_url: " << logo.metadata.source_url
<< "\nanimated_url: " << logo.metadata.animated_url
<< "\nalt_text: " << logo.metadata.alt_text
<< "\nmime_type: " << logo.metadata.mime_type
<< "\nfingerprint: " << logo.metadata.fingerprint
<< "\ncan_show_after_expiration: "
<< logo.metadata.can_show_after_expiration;
}
void PrintTo(const EncodedLogo& logo, std::ostream* ostr) {
PrintTo(DecodeLogo(logo), ostr);
}
} // namespace search_provider_logos