// 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 "chrome/browser/ash/api/tasks/tasks_client_impl.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "ash/api/tasks/tasks_types.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/glanceables/glanceables_metrics.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.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 "base/values.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/apps/app_service/publishers/app_publisher.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/services/app_service/public/cpp/app.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/browser_task_environment.h"
#include "google_apis/common/api_error_codes.h"
#include "google_apis/common/dummy_auth_service.h"
#include "google_apis/common/request_sender.h"
#include "google_apis/common/time_util.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/gaia_urls_overrider_for_testing.h"
#include "google_apis/tasks/tasks_api_requests.h"
#include "google_apis/tasks/tasks_api_task_status.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/test/test_shared_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/models/list_model.h"
#include "ui/base/models/list_model_observer.h"
namespace ash::api {
namespace {
using ::base::test::TestFuture;
using ::google_apis::ApiErrorCode;
using ::google_apis::util::FormatTimeAsString;
using ::net::test_server::BasicHttpResponse;
using ::net::test_server::HttpMethod;
using ::net::test_server::HttpRequest;
using ::net::test_server::HttpResponse;
using ::testing::_;
using ::testing::ByMove;
using ::testing::Eq;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::Invoke;
using ::testing::Not;
using ::testing::Return;
constexpr char kDefaultTaskListsResponseContent[] = R"(
{
"kind": "tasks#taskLists",
"items": [
{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
},
{
"id": "asdfgh",
"title": "My Tasks 2",
"updated": "2022-12-21T23:38:22.590Z"
}
]
}
)";
constexpr char kDefaultTasksResponseContent[] = R"(
{
"kind": "tasks#tasks",
"items": [
{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z",
"updated": "2023-01-30T22:19:22.812Z",
"webViewLink": "https://tasks.google.com/task/id1"
},
{
"id": "qwe",
"title": "Child task, level 2",
"parent": "asd",
"status": "needsAction",
"updated": "2022-12-21T23:38:22.590Z",
"webViewLink": "https://tasks.google.com/task/id2"
},
{
"id": "zxc",
"title": "Parent task 2, level 1",
"status": "needsAction",
"links": [{"type": "email"}],
"notes": "Lorem ipsum dolor sit amet",
"updated": "2022-12-21T23:38:22.590Z",
"webViewLink": "https://tasks.google.com/task/id3"
}
]
}
)";
using TaskListsFuture = TestFuture<bool,
std::optional<ApiErrorCode>,
const ui::ListModel<TaskList>*>;
using TasksFuture =
TestFuture<bool, std::optional<ApiErrorCode>, const ui::ListModel<Task>*>;
// Helper class to simplify mocking `net::EmbeddedTestServer` responses,
// especially useful for subsequent responses when testing pagination logic.
class TestRequestHandler {
public:
static std::unique_ptr<HttpResponse> CreateSuccessfulResponse(
const std::string& content) {
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content(content);
response->set_content_type("application/json");
return response;
}
static std::unique_ptr<HttpResponse> CreateFailedResponse() {
auto response = std::make_unique<BasicHttpResponse>();
response->set_code(net::HTTP_INTERNAL_SERVER_ERROR);
return response;
}
MOCK_METHOD(std::unique_ptr<HttpResponse>,
HandleRequest,
(const HttpRequest&));
};
// Observer for `ui::ListModel` changes.
class TestListModelObserver : public ui::ListModelObserver {
public:
MOCK_METHOD(void, ListItemsAdded, (size_t start, size_t count), (override));
MOCK_METHOD(void, ListItemsRemoved, (size_t start, size_t count), (override));
MOCK_METHOD(void,
ListItemMoved,
(size_t index, size_t target_index),
(override));
MOCK_METHOD(void, ListItemsChanged, (size_t start, size_t count), (override));
};
} // namespace
class TasksClientImplIsDisabledByAdminTest : public testing::Test {
public:
TasksClientImplIsDisabledByAdminTest()
: profile_manager_(
TestingProfileManager(TestingBrowserProcess::GetGlobal())) {}
void SetUp() override { ASSERT_TRUE(profile_manager_.SetUp()); }
std::unique_ptr<sync_preferences::TestingPrefServiceSyncable>
GetDefaultPrefs() const {
auto prefs =
std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
RegisterUserProfilePrefs(prefs->registry());
return prefs;
}
TestingProfile* CreateTestingProfile(
std::unique_ptr<sync_preferences::TestingPrefServiceSyncable> prefs) {
return profile_manager_.CreateTestingProfile(
"[email protected]", std::move(prefs), u"User Name", /*avatar_id=*/0,
TestingProfile::TestingFactories());
}
TasksClientImpl CreateClientForProfile(Profile* profile) const {
return TasksClientImpl(
profile,
base::BindLambdaForTesting(
[&](const std::vector<std::string>& scopes,
const net::NetworkTrafficAnnotationTag& traffic_annotation_tag)
-> std::unique_ptr<google_apis::RequestSender> {
return nullptr;
}),
TRAFFIC_ANNOTATION_FOR_TESTS);
}
const base::HistogramTester* histogram_tester() const {
return &histogram_tester_;
}
private:
content::BrowserTaskEnvironment task_environment_;
const base::HistogramTester histogram_tester_;
TestingProfileManager profile_manager_;
};
TEST_F(TasksClientImplIsDisabledByAdminTest, Default) {
auto* const profile = CreateTestingProfile(GetDefaultPrefs());
EXPECT_FALSE(CreateClientForProfile(profile).IsDisabledByAdmin());
histogram_tester()->ExpectUniqueSample(
"Ash.ContextualGoogleIntegrations.GoogleTasks.Status",
ContextualGoogleIntegrationStatus::kEnabled,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplIsDisabledByAdminTest,
NoTasksInContextualGoogleIntegrationsPref) {
auto prefs = GetDefaultPrefs();
base::Value::List enabled_integrations;
enabled_integrations.Append(prefs::kGoogleCalendarIntegrationName);
enabled_integrations.Append(prefs::kGoogleClassroomIntegrationName);
prefs->SetList(prefs::kContextualGoogleIntegrationsConfiguration,
std::move(enabled_integrations));
auto* const profile = CreateTestingProfile(std::move(prefs));
EXPECT_TRUE(CreateClientForProfile(profile).IsDisabledByAdmin());
histogram_tester()->ExpectUniqueSample(
"Ash.ContextualGoogleIntegrations.GoogleTasks.Status",
ContextualGoogleIntegrationStatus::kDisabledByPolicy,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplIsDisabledByAdminTest, DisabledCalendarApp) {
auto* const profile = CreateTestingProfile(GetDefaultPrefs());
std::vector<apps::AppPtr> app_deltas;
app_deltas.push_back(apps::AppPublisher::MakeApp(
apps::AppType::kWeb, web_app::kGoogleCalendarAppId,
apps::Readiness::kDisabledByPolicy, "Calendar",
apps::InstallReason::kUser, apps::InstallSource::kBrowser));
apps::AppServiceProxyFactory::GetForProfile(profile)->OnApps(
std::move(app_deltas), apps::AppType::kWeb,
/*should_notify_initialized=*/true);
EXPECT_TRUE(CreateClientForProfile(profile).IsDisabledByAdmin());
histogram_tester()->ExpectUniqueSample(
"Ash.ContextualGoogleIntegrations.GoogleTasks.Status",
ContextualGoogleIntegrationStatus::kDisabledByAppBlock,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplIsDisabledByAdminTest, BlockedTasksUrl) {
auto prefs = GetDefaultPrefs();
base::Value::List blocklist;
blocklist.Append("tasks.google.com");
prefs->SetManagedPref(policy::policy_prefs::kUrlBlocklist,
std::move(blocklist));
auto* const profile = CreateTestingProfile(std::move(prefs));
EXPECT_TRUE(CreateClientForProfile(profile).IsDisabledByAdmin());
histogram_tester()->ExpectUniqueSample(
"Ash.ContextualGoogleIntegrations.GoogleTasks.Status",
ContextualGoogleIntegrationStatus::kDisabledByUrlBlock,
/*expected_bucket_count=*/1);
}
class TasksClientImplTest : public testing::Test {
public:
TasksClientImplTest()
: profile_manager_(
TestingProfileManager(TestingBrowserProcess::GetGlobal())) {}
void SetUp() override {
ASSERT_TRUE(profile_manager_.SetUp());
auto create_request_sender_callback = base::BindLambdaForTesting(
[&](const std::vector<std::string>& scopes,
const net::NetworkTrafficAnnotationTag& traffic_annotation_tag) {
return std::make_unique<google_apis::RequestSender>(
std::make_unique<google_apis::DummyAuthService>(),
url_loader_factory_, task_environment_.GetMainThreadTaskRunner(),
"test-user-agent", traffic_annotation_tag);
});
client_ = std::make_unique<TasksClientImpl>(
profile_manager_.CreateTestingProfile("[email protected]",
/*is_main_profile=*/true,
url_loader_factory_),
create_request_sender_callback, TRAFFIC_ANNOTATION_FOR_TESTS);
test_server_.RegisterRequestHandler(
base::BindRepeating(&TestRequestHandler::HandleRequest,
base::Unretained(&request_handler_)));
ASSERT_TRUE(test_server_.Start());
gaia_urls_overrider_ = std::make_unique<GaiaUrlsOverriderForTesting>(
base::CommandLine::ForCurrentProcess(), "tasks_api_origin_url",
test_server_.base_url().spec());
ASSERT_EQ(GaiaUrls::GetInstance()->tasks_api_origin_url(),
test_server_.base_url().spec());
}
TasksClientImpl* client() { return client_.get(); }
base::HistogramTester* histogram_tester() { return &histogram_tester_; }
TestRequestHandler& request_handler() { return request_handler_; }
private:
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
TestingProfileManager profile_manager_;
net::EmbeddedTestServer test_server_;
scoped_refptr<network::TestSharedURLLoaderFactory> url_loader_factory_ =
base::MakeRefCounted<network::TestSharedURLLoaderFactory>(
/*network_service=*/nullptr,
/*is_trusted=*/true);
std::unique_ptr<GaiaUrlsOverriderForTesting> gaia_urls_overrider_;
testing::StrictMock<TestRequestHandler> request_handler_;
std::unique_ptr<TasksClientImpl> client_;
base::HistogramTester histogram_tester_;
};
// ----------------------------------------------------------------------------
// Get task lists:
TEST_F(TasksClientImplTest, GetTaskLists) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTaskListsResponseContent))));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, task_lists] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(task_lists->item_count(), 2u);
EXPECT_EQ(task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(task_lists->GetItemAt(0)->title, "My Tasks 1");
EXPECT_EQ(FormatTimeAsString(task_lists->GetItemAt(0)->updated),
"2023-01-30T22:19:22.812Z");
EXPECT_EQ(task_lists->GetItemAt(1)->id, "asdfgh");
EXPECT_EQ(task_lists->GetItemAt(1)->title, "My Tasks 2");
EXPECT_EQ(FormatTimeAsString(task_lists->GetItemAt(1)->updated),
"2022-12-21T23:38:22.590Z");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTaskLists.PagesCount",
/*sample=*/1,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.TaskListsCount",
/*sample=*/2,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, GetTaskListsOnSubsequentCalls) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTaskListsResponseContent))));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetRepeatingCallback());
ASSERT_TRUE(future.Wait());
const auto [status, http_error, task_lists] = future.Take();
EXPECT_TRUE(status);
// Subsequent request doesn't trigger another network call and returns a
// pointer to the same `ui::ListModel`.
client()->GetTaskLists(/*force_fetch=*/false, future.GetCallback());
ASSERT_TRUE(future.Wait());
EXPECT_EQ(std::get<2>(future.Take()), task_lists);
}
TEST_F(TasksClientImplTest, GetCachedTaskLists) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTaskListsResponseContent))));
EXPECT_EQ(client()->GetCachedTaskLists(), nullptr);
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetRepeatingCallback());
ASSERT_TRUE(future.Wait());
const auto [status, http_error, task_lists] = future.Take();
EXPECT_TRUE(status);
EXPECT_EQ(client()->GetCachedTaskLists(), task_lists);
}
TEST_F(TasksClientImplTest, ConcurrentGetTaskListsCalls) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTaskListsResponseContent))));
TaskListsFuture first_future;
client()->GetTaskLists(/*force_fetch=*/false, first_future.GetCallback());
TaskListsFuture second_future;
client()->GetTaskLists(/*force_fetch=*/false, second_future.GetCallback());
ASSERT_TRUE(first_future.Wait());
ASSERT_TRUE(second_future.Wait());
const auto [first_success, first_error, task_lists] = first_future.Take();
EXPECT_TRUE(first_success);
const auto [second_success, second_error, second_task_lists] =
second_future.Take();
EXPECT_TRUE(second_success);
EXPECT_EQ(task_lists, second_task_lists);
EXPECT_EQ(task_lists->item_count(), 2u);
EXPECT_EQ(task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(task_lists->GetItemAt(1)->id, "asdfgh");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest,
GetTaskListsOnSubsequentCallsAfterClosingGlanceablesBubble) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}, {
"id": "asdfgh",
"title": "My Tasks 2",
"updated": "2022-12-21T23:38:22.590Z"
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}, {
"id": "zxcvbn",
"title": "My Tasks 3",
"updated": "2022-12-21T23:38:22.590Z"
}]
})"))));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetRepeatingCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, task_lists] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(task_lists->item_count(), 2u);
EXPECT_EQ(task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(task_lists->GetItemAt(1)->id, "asdfgh");
client()->OnGlanceablesBubbleClosed(base::DoNothing());
// Request to get tasks after glanceables bubble was closed should trigger
// another fetch.
TaskListsFuture refresh_future;
client()->GetTaskLists(/*force_fetch=*/false, refresh_future.GetCallback());
ASSERT_TRUE(refresh_future.Wait());
const auto [refresh_success, refresh_error, refreshed_task_lists] =
refresh_future.Take();
EXPECT_TRUE(refresh_success);
EXPECT_EQ(refreshed_task_lists->item_count(), 2u);
EXPECT_EQ(refreshed_task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(refreshed_task_lists->GetItemAt(1)->id, "zxcvbn");
TaskListsFuture repeated_refresh_future;
client()->GetTaskLists(/*force_fetch=*/false,
repeated_refresh_future.GetCallback());
const auto [repeated_refresh_success, repeated_refresh_error,
repeated_refreshed_task_lists] = repeated_refresh_future.Take();
EXPECT_TRUE(repeated_refresh_success);
EXPECT_EQ(repeated_refreshed_task_lists->item_count(), 2u);
EXPECT_EQ(repeated_refreshed_task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(repeated_refreshed_task_lists->GetItemAt(1)->id, "zxcvbn");
}
TEST_F(TasksClientImplTest, GlanceablesBubbleClosedWhileFetchingTaskLists) {
base::RunLoop first_request_waiter;
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Invoke([&first_request_waiter](const HttpRequest&) {
first_request_waiter.Quit();
return TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}, {
"id": "asdfgh",
"title": "My Tasks 2",
"updated": "2022-12-21T23:38:22.590Z"
}]
})");
}))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}, {
"id": "zxcvbn",
"title": "My Tasks 3",
"updated": "2022-12-21T23:38:22.590Z"
}]
})"))));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetRepeatingCallback());
// Simulate bubble closure before first request response arrives.
client()->OnGlanceablesBubbleClosed(base::DoNothing());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, task_lists] = future.Take();
EXPECT_FALSE(success);
EXPECT_EQ(task_lists->item_count(), 0u);
// Wait for the first reqeust response to be generated before making the
// second request, to guard agains the case where second request gets handled
// by the test server before the first one.
first_request_waiter.Run();
// Request to get tasks after glanceables bubble was closed should trigger
// another fetch.
TaskListsFuture refresh_future;
client()->GetTaskLists(/*force_fetch=*/false, refresh_future.GetCallback());
ASSERT_TRUE(refresh_future.Wait());
const auto [refresh_success, refresh_error, refreshed_task_lists] =
refresh_future.Take();
EXPECT_TRUE(refresh_success);
EXPECT_EQ(refreshed_task_lists->item_count(), 2u);
EXPECT_EQ(refreshed_task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(refreshed_task_lists->GetItemAt(1)->id, "zxcvbn");
TaskListsFuture repeated_refresh_future;
client()->GetTaskLists(/*force_fetch=*/false,
repeated_refresh_future.GetCallback());
const auto [repeated_refresh_success, repeated_error,
repeated_refreshed_task_lists] = repeated_refresh_future.Take();
EXPECT_TRUE(repeated_refresh_success);
EXPECT_EQ(repeated_refreshed_task_lists->item_count(), 2u);
EXPECT_EQ(repeated_refreshed_task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(repeated_refreshed_task_lists->GetItemAt(1)->id, "zxcvbn");
}
TEST_F(TasksClientImplTest, GetTaskListsReturnsEmptyVectorOnHttpError) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, task_lists] = future.Take();
EXPECT_FALSE(success);
EXPECT_EQ(task_lists->item_count(), 0u);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, GetTaskListsReturnsCachedResultsOnHttpError) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}, {
"id": "asdfgh",
"title": "My Tasks 2",
"updated": "2022-12-21T23:38:22.590Z"
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "zxcvb",
"title": "My Tasks 3",
"updated": "2023-01-30T22:19:22.812Z"
}]
})"))));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, task_lists] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(task_lists->item_count(), 2u);
EXPECT_EQ(task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(task_lists->GetItemAt(1)->id, "asdfgh");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
client()->OnGlanceablesBubbleClosed(base::DoNothing());
TaskListsFuture failure_future;
client()->GetTaskLists(/*force_fetch=*/true, failure_future.GetCallback());
ASSERT_TRUE(failure_future.Wait());
const auto [failure_status, failure_error, failed_task_lists] =
failure_future.Take();
EXPECT_FALSE(failure_status);
EXPECT_EQ(failed_task_lists->item_count(), 2u);
EXPECT_EQ(failed_task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(failed_task_lists->GetItemAt(1)->id, "asdfgh");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/2);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status", 2);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
/*expected_bucket_count=*/1);
TaskListsFuture retry_future;
client()->GetTaskLists(/*force_fetch=*/false, retry_future.GetCallback());
ASSERT_TRUE(retry_future.Wait());
const auto [retry_success, retry_error, retry_task_lists] =
retry_future.Take();
EXPECT_TRUE(retry_success);
EXPECT_EQ(retry_task_lists->item_count(), 1u);
EXPECT_EQ(retry_task_lists->GetItemAt(0)->id, "zxcvb");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/3);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status", 3);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/2);
}
TEST_F(TasksClientImplTest,
GetTaskListsReturnsCachedResultsOnPartialHttpError) {
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
Not(HasSubstr("pageToken")))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}, {
"id": "asdfgh",
"title": "My Tasks 2",
"updated": "2022-12-21T23:38:22.590Z"
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "zxcvb",
"title": "My Tasks 3",
"updated": "2023-01-30T22:19:22.812Z"
}],
"nextPageToken": "tt"
})"))));
EXPECT_CALL(request_handler(),
HandleRequest(
Field(&HttpRequest::relative_url, HasSubstr("pageToken=tt"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, task_lists] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(task_lists->item_count(), 2u);
EXPECT_EQ(task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(task_lists->GetItemAt(1)->id, "asdfgh");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
client()->OnGlanceablesBubbleClosed(base::DoNothing());
TaskListsFuture failure_future;
client()->GetTaskLists(/*force_fetch=*/true, failure_future.GetCallback());
ASSERT_TRUE(failure_future.Wait());
const auto [failed_status, failed_error, failed_task_lists] =
failure_future.Take();
EXPECT_FALSE(failed_status);
EXPECT_EQ(failed_task_lists->item_count(), 2u);
EXPECT_EQ(failed_task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(failed_task_lists->GetItemAt(1)->id, "asdfgh");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Latency", /*expected_count=*/3);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status", 3);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/2);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTaskLists.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, GetTaskListsFetchesAllPages) {
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
Not(HasSubstr("pageToken")))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-1"}],
"nextPageToken": "qwe"
}
)"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=qwe"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-2"}],
"nextPageToken": "asd"
}
)"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=asd"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-3"}]
}
)"))));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [status, http_error, task_lists] = future.Take();
EXPECT_TRUE(status);
EXPECT_EQ(task_lists->item_count(), 3u);
EXPECT_EQ(task_lists->GetItemAt(0)->id, "task-list-from-page-1");
EXPECT_EQ(task_lists->GetItemAt(1)->id, "task-list-from-page-2");
EXPECT_EQ(task_lists->GetItemAt(2)->id, "task-list-from-page-3");
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTaskLists.PagesCount",
/*sample=*/3,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.TaskListsCount",
/*sample=*/3,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, GlanceablesBubbleClosedWhileFetchingTaskListsPage) {
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
Not(HasSubstr("pageToken")))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-1"}],
"nextPageToken": "qwe"
}
)"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-1-2"}],
"nextPageToken": "qwe"
}
)"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=qwe"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-2"}],
"nextPageToken": "asd"
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-2-2"}],
"nextPageToken": "asd"
})"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=asd"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{"id": "task-list-from-page-3-2"}]
}
)"))));
// Set a test callback that simulates glanceables bubble closing just after
// requesting the second task lists page.
client()->set_task_lists_request_callback_for_testing(
base::BindLambdaForTesting([&](const std::string& page_token) {
if (page_token == "qwe") {
client()->OnGlanceablesBubbleClosed(base::DoNothing());
}
}));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetRepeatingCallback());
// Note that injected tasks lists request test callback simulates bubble
// closure before returning the second task lists page. The
// `GetTaskLists(/*force_fetch=*/false, )` call should return, but it will
// contain empty tasks list, as closing the bubble cancels the fetch.
ASSERT_TRUE(future.Wait());
const auto [success, http_error, task_lists] = future.Take();
EXPECT_FALSE(success);
EXPECT_EQ(task_lists->item_count(), 0u);
client()->set_task_lists_request_callback_for_testing(
TasksClientImpl::TaskListsRequestCallback());
// Request to get tasks after glanceables bubble was closed should trigger
// another fetch.
TaskListsFuture refresh_future;
client()->GetTaskLists(/*force_fetch=*/false, refresh_future.GetCallback());
ASSERT_TRUE(refresh_future.Wait());
const auto [refresh_success, refresh_error, refreshed_task_lists] =
refresh_future.Take();
EXPECT_TRUE(refresh_success);
EXPECT_EQ(refreshed_task_lists->item_count(), 3u);
EXPECT_EQ(refreshed_task_lists->GetItemAt(0)->id, "task-list-from-page-1-2");
EXPECT_EQ(refreshed_task_lists->GetItemAt(1)->id, "task-list-from-page-2-2");
EXPECT_EQ(refreshed_task_lists->GetItemAt(2)->id, "task-list-from-page-3-2");
TaskListsFuture repeated_refresh_future;
client()->GetTaskLists(/*force_fetch=*/false,
repeated_refresh_future.GetCallback());
const auto [repeated_refresh_success, repreated_refresh_error,
repeated_refreshed_task_lists] = repeated_refresh_future.Take();
EXPECT_TRUE(repeated_refresh_success);
EXPECT_EQ(repeated_refreshed_task_lists->item_count(), 3u);
EXPECT_EQ(repeated_refreshed_task_lists->GetItemAt(0)->id,
"task-list-from-page-1-2");
EXPECT_EQ(repeated_refreshed_task_lists->GetItemAt(1)->id,
"task-list-from-page-2-2");
EXPECT_EQ(repeated_refreshed_task_lists->GetItemAt(2)->id,
"task-list-from-page-3-2");
}
TEST_F(TasksClientImplTest, AbandonedTaskListsRemovedFromCache) {
EXPECT_CALL(request_handler(), HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("lists?"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}, {
"id": "asdfgh",
"title": "My Tasks 2",
"updated": "2022-12-21T23:38:22.590Z"
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#taskLists",
"items": [{
"id": "qwerty",
"title": "My Tasks 1",
"updated": "2023-01-30T22:19:22.812Z"
}]
})"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("/qwerty/tasks?"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "zxc",
"title": "Parent task 3, level 1",
"status": "needsAction",
"links": [{"type": "email"}]
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("/asdfgh/tasks?"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{
"id": "fgh",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
TaskListsFuture future;
client()->GetTaskLists(/*force_fetch=*/false, future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [status, http_error, task_lists] = future.Take();
EXPECT_TRUE(status);
ASSERT_EQ(task_lists->item_count(), 2u);
EXPECT_EQ(task_lists->GetItemAt(0)->id, "qwerty");
EXPECT_EQ(task_lists->GetItemAt(1)->id, "asdfgh");
TasksFuture abandoned_tasks_future;
client()->GetTasks("asdfgh", /*force_fetch=*/true,
abandoned_tasks_future.GetCallback());
ASSERT_TRUE(abandoned_tasks_future.Wait());
const auto [abandoned_tasks_success, abandoned_error, abandoned_tasks] =
abandoned_tasks_future.Take();
EXPECT_TRUE(abandoned_tasks_success);
EXPECT_EQ(abandoned_tasks->item_count(), 1u);
EXPECT_EQ(abandoned_tasks->GetItemAt(0)->id, "fgh");
TasksFuture tasks_future;
client()->GetTasks("qwerty", /*force_fetch=*/true,
tasks_future.GetCallback());
ASSERT_TRUE(tasks_future.Wait());
const auto [tasks_success, tasks_error, tasks] = tasks_future.Take();
EXPECT_TRUE(tasks_success);
EXPECT_EQ(tasks->item_count(), 2u);
EXPECT_EQ(tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(tasks->GetItemAt(1)->id, "zxc");
client()->OnGlanceablesBubbleClosed(base::DoNothing());
TaskListsFuture refreshed_task_list_future;
client()->GetTaskLists(/*force_fetch=*/true,
refreshed_task_list_future.GetCallback());
ASSERT_TRUE(refreshed_task_list_future.Wait());
const auto [refresh_status, refresh_error, refreshed_task_lists] =
refreshed_task_list_future.Take();
EXPECT_TRUE(refresh_status);
EXPECT_EQ(refreshed_task_lists->item_count(), 1u);
EXPECT_EQ(refreshed_task_lists->GetItemAt(0)->id, "qwerty");
TasksFuture refreshed_abandoned_tasks_future;
client()->GetTasks("asdfgh", /*force_fetch=*/true,
refreshed_abandoned_tasks_future.GetCallback());
ASSERT_TRUE(refreshed_abandoned_tasks_future.Wait());
const auto [refresh_abandoned_tasks_success, refresh_abandoned_tasks_error,
refreshed_abandoned_tasks] =
refreshed_abandoned_tasks_future.Take();
EXPECT_FALSE(refresh_abandoned_tasks_success);
EXPECT_EQ(refreshed_abandoned_tasks->item_count(), 0u);
TasksFuture refreshed_tasks_future;
client()->GetTasks("qwerty", /*force_fetch=*/true,
refreshed_tasks_future.GetCallback());
ASSERT_TRUE(refreshed_tasks_future.Wait());
const auto [refresh_success, refresh_success_error, refreshed_tasks] =
refreshed_tasks_future.Take();
EXPECT_FALSE(refresh_success);
EXPECT_EQ(refreshed_tasks->item_count(), 2u);
EXPECT_EQ(refreshed_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(refreshed_tasks->GetItemAt(1)->id, "zxc");
}
// ----------------------------------------------------------------------------
// Get tasks:
TEST_F(TasksClientImplTest, GetTasks) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTasksResponseContent))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
ASSERT_EQ(root_tasks->item_count(), 2u);
EXPECT_EQ(root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(root_tasks->GetItemAt(0)->title, "Parent task, level 1");
EXPECT_EQ(root_tasks->GetItemAt(0)->completed, false);
EXPECT_EQ(FormatTimeAsString(root_tasks->GetItemAt(0)->due.value()),
"2023-04-19T00:00:00.000Z");
EXPECT_TRUE(root_tasks->GetItemAt(0)->has_subtasks);
EXPECT_FALSE(root_tasks->GetItemAt(0)->has_email_link);
EXPECT_FALSE(root_tasks->GetItemAt(0)->has_notes);
EXPECT_EQ(FormatTimeAsString(root_tasks->GetItemAt(0)->updated),
"2023-01-30T22:19:22.812Z");
EXPECT_EQ(root_tasks->GetItemAt(0)->web_view_link,
"https://tasks.google.com/task/id1");
EXPECT_EQ(root_tasks->GetItemAt(0)->origin_surface_type,
api::Task::OriginSurfaceType::kRegular);
EXPECT_EQ(root_tasks->GetItemAt(1)->id, "zxc");
EXPECT_EQ(root_tasks->GetItemAt(1)->title, "Parent task 2, level 1");
EXPECT_EQ(root_tasks->GetItemAt(1)->completed, false);
EXPECT_FALSE(root_tasks->GetItemAt(1)->due);
EXPECT_FALSE(root_tasks->GetItemAt(1)->has_subtasks);
EXPECT_TRUE(root_tasks->GetItemAt(1)->has_email_link);
EXPECT_TRUE(root_tasks->GetItemAt(1)->has_notes);
EXPECT_EQ(FormatTimeAsString(root_tasks->GetItemAt(1)->updated),
"2022-12-21T23:38:22.590Z");
EXPECT_EQ(root_tasks->GetItemAt(1)->web_view_link,
"https://tasks.google.com/task/id3");
EXPECT_EQ(root_tasks->GetItemAt(1)->origin_surface_type,
api::Task::OriginSurfaceType::kRegular);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.PagesCount",
/*sample=*/1,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.RawTasksCount",
/*sample=*/3,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.ProcessedTasksCount",
/*sample=*/2,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, ConcurrentGetTasksCalls) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTasksResponseContent))));
TasksFuture first_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
first_future.GetCallback());
TasksFuture second_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
second_future.GetCallback());
ASSERT_TRUE(first_future.Wait());
ASSERT_TRUE(second_future.Wait());
const auto [first_success, first_error, root_tasks] = first_future.Take();
EXPECT_TRUE(first_success);
const auto [second_success, second_error, second_root_tasks] =
second_future.Take();
EXPECT_TRUE(second_success);
EXPECT_EQ(root_tasks, second_root_tasks);
ASSERT_EQ(root_tasks->item_count(), 2u);
EXPECT_EQ(root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(root_tasks->GetItemAt(0)->title, "Parent task, level 1");
EXPECT_EQ(root_tasks->GetItemAt(0)->completed, false);
EXPECT_EQ(FormatTimeAsString(root_tasks->GetItemAt(0)->due.value()),
"2023-04-19T00:00:00.000Z");
EXPECT_TRUE(root_tasks->GetItemAt(0)->has_subtasks);
EXPECT_FALSE(root_tasks->GetItemAt(0)->has_email_link);
EXPECT_EQ(root_tasks->GetItemAt(1)->id, "zxc");
EXPECT_EQ(root_tasks->GetItemAt(1)->title, "Parent task 2, level 1");
EXPECT_EQ(root_tasks->GetItemAt(1)->completed, false);
EXPECT_FALSE(root_tasks->GetItemAt(1)->due);
EXPECT_FALSE(root_tasks->GetItemAt(1)->has_subtasks);
EXPECT_TRUE(root_tasks->GetItemAt(1)->has_email_link);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, ConcurrentGetTasksCallsForDifferentLists) {
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("test-task-list-1/tasks?"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "task-1-1",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "task-1-2",
"title": "Parent task 2, level 1",
"status": "needsAction"
}]
})"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("test-task-list-2/tasks?"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "task-2-1",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "task-2-2",
"title": "Parent task 2, level 1",
"status": "needsAction"
}]
})"))));
TasksFuture first_future;
client()->GetTasks("test-task-list-1", /*force_fetch=*/false,
first_future.GetCallback());
TasksFuture second_future;
client()->GetTasks("test-task-list-2", /*force_fetch=*/false,
second_future.GetCallback());
ASSERT_TRUE(first_future.Wait());
ASSERT_TRUE(second_future.Wait());
const auto [first_success, first_error, first_root_tasks] =
first_future.Take();
EXPECT_TRUE(first_success);
ASSERT_EQ(first_root_tasks->item_count(), 2u);
EXPECT_EQ(first_root_tasks->GetItemAt(0)->id, "task-1-1");
EXPECT_EQ(first_root_tasks->GetItemAt(1)->id, "task-1-2");
const auto [second_success, second_error, second_root_tasks] =
second_future.Take();
EXPECT_TRUE(second_success);
ASSERT_EQ(second_root_tasks->item_count(), 2u);
EXPECT_EQ(second_root_tasks->GetItemAt(0)->id, "task-2-1");
EXPECT_EQ(second_root_tasks->GetItemAt(1)->id, "task-2-2");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/2);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/2);
}
TEST_F(TasksClientImplTest, GetCachedTasks) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTasksResponseContent))));
EXPECT_EQ(client()->GetCachedTasksInTaskList("test-task-list-id"), nullptr);
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetRepeatingCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(client()->GetCachedTasksInTaskList("test-task-list-id"),
root_tasks);
}
TEST_F(TasksClientImplTest, GetTasksOnSubsequentCalls) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTasksResponseContent))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetRepeatingCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
// Subsequent request doesn't trigger another network call and returns a
// pointer to the same `ui::ListModel`.
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [retry_success, retry_error, retry_root_tasks] = future.Take();
EXPECT_TRUE(retry_success);
EXPECT_EQ(retry_root_tasks, root_tasks);
}
TEST_F(TasksClientImplTest, GetTasksOnSubsequentCallsWhenForcingFetch) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTasksResponseContent))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(
kDefaultTasksResponseContent))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/true,
future.GetRepeatingCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(root_tasks->GetItemAt(0)->title, "Parent task, level 1");
// When `force_fetch` is true, we get the updated `ListModel`.
client()->GetTasks("test-task-list-id", /*force_fetch=*/true,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [retry_success, retry_error, retry_root_tasks] = future.Take();
EXPECT_TRUE(retry_success);
}
TEST_F(TasksClientImplTest,
GetTasksOnSubsequentCallsAfterClosingGlanceablesBubble) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "qwe",
"title": "Parent task 2, level 1",
"status": "needsAction"
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "zxc",
"title": "Parent task 3, level 1",
"status": "needsAction",
"links": [{"type": "email"}]
}]
})"))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetRepeatingCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
ASSERT_EQ(root_tasks->item_count(), 2u);
EXPECT_EQ(root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(root_tasks->GetItemAt(1)->id, "qwe");
// Simulate glanceables bubble closure, which should cause the next tasks call
// to fetch fresh list of tasks.
client()->OnGlanceablesBubbleClosed(base::DoNothing());
TasksFuture refresh_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
refresh_future.GetCallback());
ASSERT_TRUE(refresh_future.Wait());
const auto [refresh_success, refresh_error, refreshed_root_tasks] =
refresh_future.Take();
EXPECT_TRUE(refresh_success);
ASSERT_EQ(refreshed_root_tasks->item_count(), 2u);
EXPECT_EQ(refreshed_root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(refreshed_root_tasks->GetItemAt(1)->id, "zxc");
TasksFuture repeated_refresh_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
repeated_refresh_future.GetCallback());
ASSERT_TRUE(repeated_refresh_future.Wait());
const auto [repeated_refresh_success, repeated_refresh_error,
repeated_refreshed_root_tasks] = repeated_refresh_future.Take();
EXPECT_TRUE(repeated_refresh_success);
ASSERT_EQ(repeated_refreshed_root_tasks->item_count(), 2u);
EXPECT_EQ(repeated_refreshed_root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(repeated_refreshed_root_tasks->GetItemAt(1)->id, "zxc");
}
TEST_F(TasksClientImplTest, GlanceablesBubbleClosedWhileFetchingTasks) {
base::RunLoop first_request_waiter;
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Invoke([&first_request_waiter](const HttpRequest&) {
first_request_waiter.Quit();
return TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "qwe",
"title": "Parent task 2, level 1",
"status": "needsAction"
}]
})");
}))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "zxc",
"title": "Parent task 3, level 1",
"status": "needsAction",
"links": [{"type": "email"}]
}]
})"))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetRepeatingCallback());
// Simulate glanceables bubble closure, which should cause the next tasks call
// to fetch fresh list of tasks.
client()->OnGlanceablesBubbleClosed(base::DoNothing());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_FALSE(success);
// Glanceables bubble was closed before receiving tasks response, so
// `GetTasks()` should have returned an empty list.
ASSERT_EQ(root_tasks->item_count(), 0u);
// Wait for the first reqeust response to be generated before making the
// second request, to guard agains the case where second request gets handled
// by the test server before the first one.
first_request_waiter.Run();
TasksFuture refresh_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
refresh_future.GetCallback());
ASSERT_TRUE(refresh_future.Wait());
const auto [refresh_success, refreshed_error, refreshed_root_tasks] =
refresh_future.Take();
EXPECT_TRUE(refresh_success);
ASSERT_EQ(refreshed_root_tasks->item_count(), 2u);
EXPECT_EQ(refreshed_root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(refreshed_root_tasks->GetItemAt(1)->id, "zxc");
TasksFuture repeated_refresh_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
repeated_refresh_future.GetCallback());
ASSERT_TRUE(repeated_refresh_future.Wait());
const auto [repeated_refresh_success, repeated_refresh_error,
repeated_refreshed_root_tasks] = repeated_refresh_future.Take();
EXPECT_TRUE(repeated_refresh_success);
ASSERT_EQ(repeated_refreshed_root_tasks->item_count(), 2u);
EXPECT_EQ(repeated_refreshed_root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(repeated_refreshed_root_tasks->GetItemAt(1)->id, "zxc");
}
TEST_F(TasksClientImplTest, GetTasksReturnsEmptyVectorOnHttpError) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_FALSE(success);
EXPECT_EQ(root_tasks->item_count(), 0u);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, GetTasksReturnsCachedResultsOnHttpError) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "zxc",
"title": "Parent task 3, level 1",
"status": "needsAction",
"links": [{"type": "email"}]
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "qwe",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}]
})"))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(root_tasks->item_count(), 2u);
EXPECT_EQ(root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(root_tasks->GetItemAt(1)->id, "zxc");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
client()->OnGlanceablesBubbleClosed(base::DoNothing());
TasksFuture failed_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/true,
failed_future.GetCallback());
ASSERT_TRUE(failed_future.Wait());
const auto [failed_status, failed_error, failed_root_tasks] =
failed_future.Take();
EXPECT_FALSE(failed_status);
EXPECT_EQ(failed_root_tasks->item_count(), 2u);
EXPECT_EQ(failed_root_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(failed_root_tasks->GetItemAt(1)->id, "zxc");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/2);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", /*expected_count=*/2);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
/*expected_bucket_count=*/1);
TasksFuture retry_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/true,
retry_future.GetCallback());
ASSERT_TRUE(retry_future.Wait());
const auto [retry_success, retry_error, retry_root_tasks] =
retry_future.Take();
EXPECT_TRUE(retry_success);
EXPECT_EQ(retry_root_tasks->item_count(), 1u);
EXPECT_EQ(retry_root_tasks->GetItemAt(0)->id, "qwe");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/3);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", /*expected_count=*/3);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/2);
}
TEST_F(TasksClientImplTest, GetTasksReturnsCachedResultsOnPartialHttpError) {
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
Not(HasSubstr("pageToken")))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{
"id": "asd",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}, {
"id": "zxc",
"title": "Parent task 3, level 1",
"status": "needsAction",
"links": [{"type": "email"}]
}]
})"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"({
"kind": "tasks#tasks",
"items": [{
"id": "qwe",
"title": "Parent task, level 1",
"status": "needsAction",
"due": "2023-04-19T00:00:00.000Z"
}],
"nextPageToken": "tt"
})"))));
EXPECT_CALL(request_handler(),
HandleRequest(
Field(&HttpRequest::relative_url, HasSubstr("pageToken=tt"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
TasksFuture future;
client()->GetTasks("task-list-1", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, tasks] = future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(tasks->item_count(), 2u);
EXPECT_EQ(tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(tasks->GetItemAt(1)->id, "zxc");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
client()->OnGlanceablesBubbleClosed(base::DoNothing());
TasksFuture failure_future;
client()->GetTasks("task-list-1", /*force_fetch=*/true,
failure_future.GetCallback());
ASSERT_TRUE(failure_future.Wait());
const auto [failure_status, failure_error, failed_tasks] =
failure_future.Take();
EXPECT_FALSE(failure_status);
EXPECT_EQ(failed_tasks->item_count(), 2u);
EXPECT_EQ(failed_tasks->GetItemAt(0)->id, "asd");
EXPECT_EQ(failed_tasks->GetItemAt(1)->id, "zxc");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Latency", /*expected_count=*/3);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", 3);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/2);
histogram_tester()->ExpectBucketCount(
"Ash.Glanceables.Api.Tasks.GetTasks.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, GetTasksFetchesAllPages) {
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
Not(HasSubstr("pageToken")))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [
{
"id": "child-task-from-page-1",
"parent": "parent-task-from-page-2"
}
],
"nextPageToken": "qwe"
}
)"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=qwe"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{"id": "parent-task-from-page-2"}],
"nextPageToken": "asd"
}
)"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=asd"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{"id": "parent-task-from-page-3"}]
}
)"))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
ASSERT_EQ(root_tasks->item_count(), 2u);
EXPECT_EQ(root_tasks->GetItemAt(0)->id, "parent-task-from-page-2");
EXPECT_TRUE(root_tasks->GetItemAt(0)->has_subtasks);
EXPECT_EQ(root_tasks->GetItemAt(1)->id, "parent-task-from-page-3");
EXPECT_FALSE(root_tasks->GetItemAt(1)->has_subtasks);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.GetTasks.PagesCount",
/*sample=*/3,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.RawTasksCount",
/*sample=*/3,
/*expected_bucket_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.ProcessedTasksCount",
/*sample=*/2,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, GlanceablesBubbleClosedWhileFetchingTasksPage) {
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
Not(HasSubstr("pageToken")))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{"id": "task-from-page-1"}],
"nextPageToken": "qwe"
}
)"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{"id": "task-from-page-1-2"}],
"nextPageToken": "qwe"
}
)"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=qwe"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{"id": "task-from-page-2"}],
"nextPageToken": "asd"
}
)"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{"id": "task-from-page-2-2"}],
"nextPageToken": "asd"
}
)"))));
EXPECT_CALL(request_handler(),
HandleRequest(Field(&HttpRequest::relative_url,
HasSubstr("pageToken=asd"))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [{"id": "task-from-page-3-2"}]
}
)"))));
// Inject a test callback that will simulate glanceables bubble closure after
// requesting second page of tasks.
client()->set_tasks_request_callback_for_testing(base::BindLambdaForTesting(
[&](const std::string& task_list_id, const std::string& page_token) {
ASSERT_EQ("test-task-list-id", task_list_id);
if (page_token == "qwe") {
client()->OnGlanceablesBubbleClosed(base::DoNothing());
}
}));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
// Expect an empty list, given that the glanceables bubble got closed before
// all tasks were fetched, effectively cancelling the fetch.
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_FALSE(success);
ASSERT_EQ(root_tasks->item_count(), 0u);
client()->set_tasks_request_callback_for_testing(
TasksClientImpl::TasksRequestCallback());
TasksFuture refresh_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
refresh_future.GetCallback());
ASSERT_TRUE(refresh_future.Wait());
const auto [refresh_success, refresh_error, refreshed_root_tasks] =
refresh_future.Take();
EXPECT_TRUE(refresh_success);
ASSERT_EQ(refreshed_root_tasks->item_count(), 3u);
EXPECT_EQ(refreshed_root_tasks->GetItemAt(0)->id, "task-from-page-1-2");
EXPECT_EQ(refreshed_root_tasks->GetItemAt(1)->id, "task-from-page-2-2");
EXPECT_EQ(refreshed_root_tasks->GetItemAt(2)->id, "task-from-page-3-2");
TasksFuture repeated_refresh_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
repeated_refresh_future.GetCallback());
ASSERT_TRUE(repeated_refresh_future.Wait());
const auto [repeated_refresh_success, repeated_refresh_error,
repeated_refreshed_root_tasks] = repeated_refresh_future.Take();
EXPECT_TRUE(repeated_refresh_success);
ASSERT_EQ(repeated_refreshed_root_tasks->item_count(), 3u);
EXPECT_EQ(repeated_refreshed_root_tasks->GetItemAt(0)->id,
"task-from-page-1-2");
EXPECT_EQ(repeated_refreshed_root_tasks->GetItemAt(1)->id,
"task-from-page-2-2");
EXPECT_EQ(repeated_refreshed_root_tasks->GetItemAt(2)->id,
"task-from-page-3-2");
}
TEST_F(TasksClientImplTest, GetTasksSortsByPosition) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [
{"title": "2nd", "position": "00000000000000000001"},
{"title": "3rd", "position": "00000000000000000002"},
{"title": "1st", "position": "00000000000000000000"}
]
}
)"))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
ASSERT_EQ(root_tasks->item_count(), 3u);
EXPECT_EQ(root_tasks->GetItemAt(0)->title, "1st");
EXPECT_EQ(root_tasks->GetItemAt(1)->title, "2nd");
EXPECT_EQ(root_tasks->GetItemAt(2)->title, "3rd");
}
TEST_F(TasksClientImplTest, GetTasksHandlesOriginSurfaceType) {
EXPECT_CALL(request_handler(), HandleRequest(_))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [
{"id": "1"},
{"id": "2", "assignmentInfo": {"surfaceType": "DOCUMENT"}},
{"id": "3", "assignmentInfo": {"surfaceType": "SPACE"}},
{"id": "4", "assignmentInfo": {"surfaceType": "UNKNOWN"}}
]
}
)"))));
TasksFuture future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
future.GetCallback());
ASSERT_TRUE(future.Wait());
const auto [success, http_error, root_tasks] = future.Take();
EXPECT_TRUE(success);
ASSERT_EQ(root_tasks->item_count(), 4u);
EXPECT_EQ(root_tasks->GetItemAt(0)->origin_surface_type,
api::Task::OriginSurfaceType::kRegular);
EXPECT_EQ(root_tasks->GetItemAt(1)->origin_surface_type,
api::Task::OriginSurfaceType::kDocument);
EXPECT_EQ(root_tasks->GetItemAt(2)->origin_surface_type,
api::Task::OriginSurfaceType::kSpace);
EXPECT_EQ(root_tasks->GetItemAt(3)->origin_surface_type,
api::Task::OriginSurfaceType::kUnknown);
}
// ----------------------------------------------------------------------------
// Mark as completed:
TEST_F(TasksClientImplTest, MarkAsCompleted) {
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_GET))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [
{
"id": "task-1",
"status": "needsAction"
},
{
"id": "task-2",
"status": "needsAction"
}
]
}
)"))));
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_PATCH))))
.Times(2)
.WillRepeatedly(Invoke([](const HttpRequest&) {
return TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#task",
"id": "task-id",
"title": "Updated title",
"status": "completed"
}
)");
}));
TasksFuture get_tasks_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
get_tasks_future.GetCallback());
ASSERT_TRUE(get_tasks_future.Wait());
const auto [success, http_error, tasks] = get_tasks_future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(tasks->item_count(), 2u);
TestFuture<bool> mark_as_completed_future;
client()->MarkAsCompleted("test-task-list-id", "task-1", true);
client()->MarkAsCompleted("test-task-list-id", "task-2", true);
TestFuture<void> glanceables_bubble_closed_future;
client()->OnGlanceablesBubbleClosed(
glanceables_bubble_closed_future.GetCallback());
ASSERT_TRUE(glanceables_bubble_closed_future.Wait());
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Latency", /*expected_count=*/2);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.PatchTask.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/2);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.SimultaneousMarkAsCompletedRequestsCount",
/*sample=*/2,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, MarkAsCompletedOnHttpError) {
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_GET))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [
{
"id": "task-1",
"status": "needsAction"
},
{
"id": "task-2",
"status": "needsAction"
}
]
}
)"))));
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_PATCH))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
TasksFuture get_tasks_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
get_tasks_future.GetCallback());
ASSERT_TRUE(get_tasks_future.Wait());
const auto [success, http_error, tasks] = get_tasks_future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(tasks->item_count(), 2u);
client()->MarkAsCompleted("test-task-list-id", "task-2", true);
EXPECT_EQ(tasks->item_count(), 2u);
TestFuture<void> glanceables_bubble_closed_future;
client()->OnGlanceablesBubbleClosed(
glanceables_bubble_closed_future.GetCallback());
ASSERT_TRUE(glanceables_bubble_closed_future.Wait());
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.PatchTask.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR, /*expected_bucket_count=*/1);
}
// ----------------------------------------------------------------------------
// Add a new task:
TEST_F(TasksClientImplTest, AddsNewTask) {
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_GET))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [
{
"id": "task-id",
"title": "Task 1",
"status": "needsAction"
}
]
}
)"))));
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_POST))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#task",
"id": "new-task-id",
"title": "New task"
}
)"))));
TasksFuture get_tasks_future;
client()->GetTasks("test-task-list-id", /*force_fetch=*/false,
get_tasks_future.GetCallback());
ASSERT_TRUE(get_tasks_future.Wait());
const auto [success, http_error, tasks] = get_tasks_future.Take();
EXPECT_TRUE(success);
EXPECT_EQ(tasks->item_count(), 1u);
EXPECT_EQ(tasks->GetItemAt(0)->id, "task-id");
EXPECT_EQ(tasks->GetItemAt(0)->title, "Task 1");
testing::StrictMock<TestListModelObserver> observer;
tasks->AddObserver(&observer);
EXPECT_CALL(observer, ListItemsAdded(/*start=*/0, /*count=*/1));
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.InsertTask.Latency", /*expected_count=*/0);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.InsertTask.Status", /*expected_count=*/0);
TestFuture<ApiErrorCode, const Task*> add_task_future;
client()->AddTask("test-task-list-id", "New task",
add_task_future.GetCallback());
ASSERT_TRUE(add_task_future.Wait());
const auto [new_error, new_task] = add_task_future.Take();
EXPECT_EQ(new_task->id, "new-task-id");
EXPECT_EQ(new_task->title, "New task");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.InsertTask.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.InsertTask.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
}
// ----------------------------------------------------------------------------
// Update a task:
TEST_F(TasksClientImplTest, UpdatesTask) {
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_GET))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#tasks",
"items": [
{
"id": "task-id",
"title": "Task 1",
"status": "needsAction"
}
]
}
)"))));
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_PATCH))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateSuccessfulResponse(R"(
{
"kind": "tasks#task",
"id": "task-id",
"title": "Updated title",
"status": "completed"
}
)"))));
// Get tasks first.
TasksFuture get_tasks_future;
client()->GetTasks("task-list-id", /*force_fetch=*/false,
get_tasks_future.GetCallback());
ASSERT_TRUE(get_tasks_future.Wait());
const auto [success, http_error, tasks] = get_tasks_future.Take();
EXPECT_TRUE(success);
ASSERT_EQ(tasks->item_count(), 1u);
EXPECT_EQ(tasks->GetItemAt(0)->title, "Task 1");
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Latency", /*expected_count=*/0);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Status", /*expected_count=*/0);
// Update the task.
TestFuture<ApiErrorCode, const Task*> update_task_future;
client()->UpdateTask("task-list-id", "task-id", "Updated title",
/*completed=*/true, update_task_future.GetCallback());
ASSERT_TRUE(update_task_future.Wait());
// Make sure `tasks` contains the update.
EXPECT_EQ(tasks->GetItemAt(0), std::get<1>(update_task_future.Take()));
EXPECT_EQ(tasks->GetItemAt(0)->title, "Updated title");
EXPECT_EQ(tasks->GetItemAt(0)->completed, true);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.PatchTask.Status", ApiErrorCode::HTTP_SUCCESS,
/*expected_bucket_count=*/1);
}
TEST_F(TasksClientImplTest, UpdatesTaskOnHttpError) {
EXPECT_CALL(
request_handler(),
HandleRequest(Field(&HttpRequest::method, Eq(HttpMethod::METHOD_PATCH))))
.WillOnce(Return(ByMove(TestRequestHandler::CreateFailedResponse())));
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Latency", /*expected_count=*/0);
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Status", /*expected_count=*/0);
TestFuture<ApiErrorCode, const Task*> update_task_future;
client()->UpdateTask("task-list-id", "task-id", "Updated title",
/*completed=*/false, update_task_future.GetCallback());
ASSERT_TRUE(update_task_future.Wait());
EXPECT_FALSE(std::get<1>(update_task_future.Take()));
histogram_tester()->ExpectTotalCount(
"Ash.Glanceables.Api.Tasks.PatchTask.Latency", /*expected_count=*/1);
histogram_tester()->ExpectUniqueSample(
"Ash.Glanceables.Api.Tasks.PatchTask.Status",
ApiErrorCode::HTTP_INTERNAL_SERVER_ERROR,
/*expected_bucket_count=*/1);
}
} // namespace ash::api