blob: cc94db05d2c6e6bec325d8e3ff52dab4f97d8447 [file] [log] [blame]
// Copyright 2020 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 <memory>
#include "base/hash/hash.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "chrome/browser/search/task_module/task_module_service.h"
#include "chrome/test/base/testing_profile.h"
#include "components/search/ntp_features.h"
#include "components/variations/scoped_variations_ids_provider.h"
#include "content/public/test/browser_task_environment.h"
#include "net/base/url_util.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
class TaskModuleServiceTest : public testing::Test {
public:
TaskModuleServiceTest()
: task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP,
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void SetUp() override {
testing::Test::SetUp();
service_ = std::make_unique<TaskModuleService>(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_),
&profile_, "en-US");
}
void TearDown() override {
service_.reset();
test_url_loader_factory_.ClearResponses();
}
protected:
// Required to run tests from UI thread.
content::BrowserTaskEnvironment task_environment_;
variations::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
variations::VariationsIdsProvider::Mode::kUseSignedInState};
TestingProfile profile_;
network::TestURLLoaderFactory test_url_loader_factory_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
base::HistogramTester histogram_tester_;
std::unique_ptr<TaskModuleService> service_;
};
// Verifies correct parsing of well-formed JSON.
TEST_F(TaskModuleServiceTest, GoodShoppingResponse) {
auto fiveMonthsAgoTimestamp =
(base::Time::Now() - base::TimeDelta::FromDays(165) -
base::Time::UnixEpoch())
.InSeconds();
auto twoDaysAgoTimestamp = (base::Time::Now() - base::TimeDelta::FromDays(2) -
base::Time::UnixEpoch())
.InSeconds();
auto nowTimestamp = (base::Time::Now() - base::Time::UnixEpoch()).InSeconds();
auto inTwoDaysTimestamp = (base::Time::Now() + base::TimeDelta::FromDays(2) -
base::Time::UnixEpoch())
.InSeconds();
test_url_loader_factory_.AddResponse(
"https://www.google.com/async/newtab_shopping_tasks?hl=en-US",
base::StringPrintf(R"()]}'
{
"update": {
"shopping_tasks": [
{
"title": "hello world",
"task_name": "hello world",
"products": [
{
"name": "foo",
"image_url": "https://foo.com",
"price": "$500",
"viewed_timestamp": {
"seconds": %s
},
"target_url": "https://google.com/foo"
},{
"name": "bar",
"image_url": "https://bar.com",
"price": "$400",
"viewed_timestamp": {
"seconds": %s
},
"target_url": "https://google.com/bar"
},{
"name": "baz",
"image_url": "https://baz.com",
"price": "$500",
"viewed_timestamp": {
"seconds": %s
},
"target_url": "https://google.com/baz"
},{
"name": "blub",
"image_url": "https://blub.com",
"price": "$600",
"viewed_timestamp": {
"seconds": %s
},
"target_url": "https://google.com/blub"
}
],
"related_searches": [
{
"text": "baz",
"target_url": "https://google.com/baz"
},
{
"text": "blub",
"target_url": "https://google.com/blub"
}
]
}
]
}
})",
base::NumberToString(fiveMonthsAgoTimestamp).c_str(),
base::NumberToString(twoDaysAgoTimestamp).c_str(),
base::NumberToString(nowTimestamp).c_str(),
base::NumberToString(inTwoDaysTimestamp).c_str()));
task_module::mojom::TaskPtr result;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback;
EXPECT_CALL(callback, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result](task_module::mojom::TaskPtr arg) {
result = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback.Get());
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(result);
EXPECT_EQ("hello world", result->title);
EXPECT_EQ(4ul, result->task_items.size());
EXPECT_EQ(2ul, result->related_searches.size());
EXPECT_EQ("foo", result->task_items[0]->name);
EXPECT_EQ("https://foo.com/", result->task_items[0]->image_url.spec());
EXPECT_EQ("$500", result->task_items[0]->price);
EXPECT_EQ("Viewed 5 months ago", result->task_items[0]->info);
EXPECT_EQ("https://google.com/foo", result->task_items[0]->target_url.spec());
EXPECT_EQ("bar", result->task_items[1]->name);
EXPECT_EQ("https://bar.com/", result->task_items[1]->image_url.spec());
EXPECT_EQ("$400", result->task_items[1]->price);
EXPECT_EQ("Viewed 2 days ago", result->task_items[1]->info);
EXPECT_EQ("https://google.com/bar", result->task_items[1]->target_url.spec());
EXPECT_EQ("baz", result->related_searches[0]->text);
EXPECT_EQ("https://google.com/baz",
result->related_searches[0]->target_url.spec());
EXPECT_EQ("blub", result->related_searches[1]->text);
EXPECT_EQ("https://google.com/blub",
result->related_searches[1]->target_url.spec());
ASSERT_EQ(1, histogram_tester_.GetBucketCount(
"NewTabPage.Modules.DataRequest",
base::PersistentHash("shopping_tasks")));
EXPECT_EQ("Viewed today", result->task_items[2]->info);
EXPECT_EQ("Viewed today", result->task_items[3]->info);
}
// Verifies service can handle multiple in flight requests.
TEST_F(TaskModuleServiceTest, MultiRequest) {
test_url_loader_factory_.AddResponse(
"https://www.google.com/async/newtab_shopping_tasks?hl=en-US",
R"()]}'
{
"update": {
"shopping_tasks": [
{
"title": "hello world",
"task_name": "hello world",
"products": [
{
"name": "foo",
"image_url": "https://foo.com",
"price": "$500",
"viewed_timestamp": {
"seconds": 123
},
"target_url": "https://google.com/foo"
}
],
"related_searches": [
{
"text": "baz",
"target_url": "https://google.com/baz"
}
]
}
]
}
})");
task_module::mojom::TaskPtr result1;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback1;
EXPECT_CALL(callback1, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result1](task_module::mojom::TaskPtr arg) {
result1 = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback1.Get());
task_module::mojom::TaskPtr result2;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback2;
EXPECT_CALL(callback2, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result2](task_module::mojom::TaskPtr arg) {
result2 = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback2.Get());
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(result1);
EXPECT_TRUE(result2);
ASSERT_EQ(2, histogram_tester_.GetBucketCount(
"NewTabPage.Modules.DataRequest",
base::PersistentHash("shopping_tasks")));
}
// Verifies error if JSON is malformed.
TEST_F(TaskModuleServiceTest, BadShoppingResponse) {
test_url_loader_factory_.AddResponse(
"https://www.google.com/async/newtab_shopping_tasks?hl=en-US",
")]}'{\"update\":{\"promotions\":{}}}");
task_module::mojom::TaskPtr result;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback;
EXPECT_CALL(callback, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result](task_module::mojom::TaskPtr arg) {
result = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback.Get());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(result);
ASSERT_EQ(1, histogram_tester_.GetBucketCount(
"NewTabPage.Modules.DataRequest",
base::PersistentHash("shopping_tasks")));
}
// Verifies error if no products.
TEST_F(TaskModuleServiceTest, NoTaskItems) {
test_url_loader_factory_.AddResponse(
"https://www.google.com/async/newtab_shopping_tasks?hl=en-US",
R"()]}'
{
"update": {
"shopping_tasks": [
{
"title": "hello world",
"task_name": "hello world",
"products": [],
"related_searches": [
{
"text": "baz",
"target_url": "https://google.com/baz"
}
]
}
]
}
})");
task_module::mojom::TaskPtr result;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback;
EXPECT_CALL(callback, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result](task_module::mojom::TaskPtr arg) {
result = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback.Get());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(result);
ASSERT_EQ(1, histogram_tester_.GetBucketCount(
"NewTabPage.Modules.DataRequest",
base::PersistentHash("shopping_tasks")));
}
// Verifies error if download fails.
TEST_F(TaskModuleServiceTest, ErrorResponse) {
test_url_loader_factory_.AddResponse(
GURL("https://www.google.com/async/newtab_shopping_tasks?hl=en-US"),
network::mojom::URLResponseHead::New(), std::string(),
network::URLLoaderCompletionStatus(net::HTTP_NOT_FOUND));
task_module::mojom::TaskPtr result;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback;
EXPECT_CALL(callback, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result](task_module::mojom::TaskPtr arg) {
result = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback.Get());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(result);
ASSERT_EQ(1, histogram_tester_.GetBucketCount(
"NewTabPage.Modules.DataRequest",
base::PersistentHash("shopping_tasks")));
}
// Verifies shopping tasks can be dismissed and restored and that the service
// remembers not to return dismissed tasks.
TEST_F(TaskModuleServiceTest, DismissTasks) {
test_url_loader_factory_.AddResponse(
"https://www.google.com/async/newtab_shopping_tasks?hl=en-US",
R"()]}'
{
"update": {
"shopping_tasks": [
{
"title": "task 1 title",
"task_name": "task 1 name",
"products": [
{
"name": "foo",
"image_url": "https://foo.com",
"price": "$500",
"viewed_timestamp": {
"seconds": 123
},
"target_url": "https://google.com/foo"
}
],
"related_searches": [
{
"text": "baz",
"target_url": "https://google.com/baz"
}
]
},
{
"title": "task 2 title",
"task_name": "task 2 name",
"products": [
{
"name": "foo",
"image_url": "https://foo.com",
"price": "$500",
"viewed_timestamp": {
"seconds": 123
},
"target_url": "https://google.com/foo"
}
],
"related_searches": [
{
"text": "baz",
"target_url": "https://google.com/baz"
}
]
}
]
}
})");
task_module::mojom::TaskPtr result1;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback1;
EXPECT_CALL(callback1, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result1](task_module::mojom::TaskPtr arg) {
result1 = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback1.Get());
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(result1);
EXPECT_EQ("task 1 name", result1->name);
service_->DismissTask(task_module::mojom::TaskModuleType::kShopping,
"task 1 name");
task_module::mojom::TaskPtr result2;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback2;
EXPECT_CALL(callback2, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result2](task_module::mojom::TaskPtr arg) {
result2 = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback2.Get());
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(result2);
EXPECT_EQ("task 2 name", result2->name);
service_->DismissTask(task_module::mojom::TaskModuleType::kShopping,
"task 2 name");
task_module::mojom::TaskPtr result3;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback3;
EXPECT_CALL(callback3, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result3](task_module::mojom::TaskPtr arg) {
result3 = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback3.Get());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(result3);
service_->RestoreTask(task_module::mojom::TaskModuleType::kShopping,
"task 2 name");
task_module::mojom::TaskPtr result4;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback4;
EXPECT_CALL(callback4, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result4](task_module::mojom::TaskPtr arg) {
result4 = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback4.Get());
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(result4);
EXPECT_EQ("task 2 name", result4->name);
service_->RestoreTask(task_module::mojom::TaskModuleType::kShopping,
"task 1 name");
task_module::mojom::TaskPtr result5;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback5;
EXPECT_CALL(callback5, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke([&result5](task_module::mojom::TaskPtr arg) {
result5 = std::move(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback5.Get());
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(result5);
EXPECT_EQ("task 1 name", result5->name);
ASSERT_EQ(5, histogram_tester_.GetBucketCount(
"NewTabPage.Modules.DataRequest",
base::PersistentHash("shopping_tasks")));
}
// Verifies caching param is added if requested.
TEST_F(TaskModuleServiceTest, CachingParam) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeaturesAndParameters(
{{ntp_features::kNtpShoppingTasksModule,
{{ntp_features::kNtpShoppingTasksModuleCacheMaxAgeSParam, "123"}}}},
{});
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
TaskModuleService::TaskModuleCallback());
base::RunLoop().RunUntilIdle();
GURL url = test_url_loader_factory_.pending_requests()->at(0).request.url;
std::string async_param;
net::GetValueForKeyInQuery(url, "async", &async_param);
EXPECT_EQ("cache_max_age_s:123", async_param);
}
// Verifies no caching param is added if not requested.
TEST_F(TaskModuleServiceTest, NoCachingParam) {
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
TaskModuleService::TaskModuleCallback());
base::RunLoop().RunUntilIdle();
GURL url = test_url_loader_factory_.pending_requests()->at(0).request.url;
std::string async_param;
net::GetValueForKeyInQuery(url, "async", &async_param);
EXPECT_EQ("", async_param);
}
// Verifies that no data request is logged if load comes from cache.
TEST_F(TaskModuleServiceTest, NoLogIfCached) {
network::URLLoaderCompletionStatus status;
status.exists_in_cache = true;
test_url_loader_factory_.AddResponse(
GURL("https://www.google.com/async/newtab_shopping_tasks?hl=en-US"),
network::CreateURLResponseHead(net::HTTP_OK),
R"()]}'
{
"update": {
"shopping_tasks": [
{
"title": "hello world",
"task_name": "hello world",
"products": [
{
"name": "foo",
"image_url": "https://foo.com",
"price": "$500",
"viewed_timestamp": {
"seconds": 123
},
"target_url": "https://google.com/foo"
}
],
"related_searches": [
{
"text": "baz",
"target_url": "https://google.com/baz"
}
]
}
]
}
})",
status);
bool received_response = false;
base::MockCallback<TaskModuleService::TaskModuleCallback> callback;
EXPECT_CALL(callback, Run(testing::_))
.Times(1)
.WillOnce(testing::Invoke(
[&received_response](task_module::mojom::TaskPtr arg) {
received_response = static_cast<bool>(arg);
}));
service_->GetPrimaryTask(task_module::mojom::TaskModuleType::kShopping,
callback.Get());
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(received_response);
EXPECT_EQ(0, histogram_tester_.GetBucketCount(
"NewTabPage.Modules.DataRequest",
base::PersistentHash("shopping_tasks")));
}