// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/time/calendar_event_fetch.h"
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include "ash/calendar/calendar_client.h"
#include "ash/calendar/calendar_controller.h"
#include "ash/shell.h"
#include "ash/system/power/peripheral_battery_listener.h"
#include "ash/system/time/calendar_event_fetch_types.h"
#include "ash/system/time/calendar_unittest_utils.h"
#include "ash/system/time/calendar_utils.h"
#include "ash/test/ash_test_base.h"
#include "ash/test_shell_delegate.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/account_id/account_id.h"
#include "google_apis/common/api_error_codes.h"
namespace ash {
// Subclass of CalendarClient where we control the results of a call to
// GetEventList(), which is what's being called downstream when a
// CalendarEventFetch is instantiated.
class TestCalendarClient : public CalendarClient {
public:
TestCalendarClient() = default;
TestCalendarClient(const TestCalendarClient& other) = delete;
TestCalendarClient& operator=(const TestCalendarClient& other) = delete;
~TestCalendarClient() override = default;
bool IsDisabledByAdmin() const override { return false; }
base::OnceClosure GetCalendarList(
google_apis::calendar::CalendarListCallback callback) override {
// This method should not be used during the event fetch unit test.
return base::DoNothing();
}
base::OnceClosure GetEventList(
google_apis::calendar::CalendarEventListCallback callback,
const base::Time start_time,
const base::Time end_time) override {
// Store these off.
start_time_ = start_time;
callback_ = std::move(callback);
// By delaying the response, we make the unit tests behave a little more
// like a event fetch in production. Use set_response_delay() to use a
// value that's different from the default.
StartResponseDelayTimeout();
return base::BindOnce(&TestCalendarClient::CancelCallback,
weak_factory_.GetWeakPtr());
}
base::OnceClosure GetEventList(
google_apis::calendar::CalendarEventListCallback callback,
const base::Time start_time,
const base::Time end_time,
const std::string& calendar_id,
const std::string& calendar_color_id) override {
// Store these off.
start_time_ = start_time;
callback_ = std::move(callback);
calendar_id_ = calendar_id;
// By delaying the response, we make the unit tests behave a little more
// like a event fetch in production. Use set_response_delay() to use a
// value that's different from the default.
StartResponseDelayTimeout();
return base::BindOnce(&TestCalendarClient::CancelCallback,
weak_factory_.GetWeakPtr());
}
void CancelCallback() { set_api_error_code(google_apis::CANCELLED); }
void set_event_list(
std::unique_ptr<google_apis::calendar::EventList> event_list) {
event_list_ = std::move(event_list);
}
void set_api_error_code(google_apis::ApiErrorCode api_error_code) {
api_error_code_ = api_error_code;
}
void set_response_delay(const base::TimeDelta& delay) {
response_delay_ = delay;
}
const base::TimeDelta get_response_delay() { return response_delay_; }
private:
void StartResponseDelayTimeout() {
fetch_response_timeout_.Start(
FROM_HERE, response_delay_,
base::BindRepeating(&TestCalendarClient::OnResponseDelayTimeout,
base::Unretained(this)));
}
void OnResponseDelayTimeout() {
// We send back an event list, possibly empty, with every response.
auto requested_event_list =
std::make_unique<google_apis::calendar::EventList>();
requested_event_list->set_time_zone("Greenwich Mean Time");
// If we're set to return an error.
if (api_error_code_ != google_apis::HTTP_SUCCESS) {
std::move(callback_).Run(api_error_code_,
std::move(requested_event_list));
return;
}
// If we have some events, send back any that start in the month we
// requested.
if (event_list_) {
for (auto& event : event_list_->items()) {
if (calendar_test_utils::IsTheSameMonth(event->start_time().date_time(),
start_time_)) {
requested_event_list->InjectItemForTesting(
calendar_test_utils::CreateEvent(event->id().c_str(),
event->summary().c_str(),
event->start_time().date_time(),
event->end_time().date_time()));
}
}
}
std::move(callback_).Run(api_error_code_, std::move(requested_event_list));
}
google_apis::calendar::CalendarEventListCallback callback_;
base::Time start_time_;
std::optional<std::string> calendar_id_;
std::unique_ptr<google_apis::calendar::EventList> event_list_;
google_apis::ApiErrorCode api_error_code_ = google_apis::HTTP_SUCCESS;
base::RetainingOneShotTimer fetch_response_timeout_;
base::TimeDelta response_delay_ = base::Milliseconds(100);
base::WeakPtrFactory<TestCalendarClient> weak_factory_{this};
};
class CalendarEventFetchTest : public NoSessionAshTestBase {
public:
CalendarEventFetchTest()
: NoSessionAshTestBase(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
CalendarEventFetchTest(const CalendarEventFetchTest& other) = delete;
CalendarEventFetchTest& operator=(const CalendarEventFetchTest& other) =
delete;
~CalendarEventFetchTest() override = default;
void RegisterClient() {
DCHECK(Shell::HasInstance());
Shell::Get()->calendar_controller()->SetActiveUserAccountIdForTesting(
GetDefaultUserId());
Shell::Get()->calendar_controller()->RegisterClientForUser(
GetDefaultUserId(),
/*client=*/&client_);
}
// Actual callback invoked when an event fetch is complete.
void OnEventsFetched(base::Time start_of_month,
std::string calendar_id,
google_apis::ApiErrorCode error,
const google_apis::calendar::EventList* events) {
calendar_id_ = calendar_id;
api_error_code_ = error;
events_fetched_count_ = 0;
if (events)
events_fetched_count_ = events->items().size();
}
// Callback invoked when an event fetch failed with an internal error.
void OnEventFetchFailedInternalError(
base::Time start_of_month,
std::string calendar_id,
CalendarEventFetchInternalErrorCode error) {
calendar_id_ = calendar_id;
internal_error_code_ = error;
}
base::Time GetStartOfMonthFromString(const char* str) {
base::Time start_of_month;
bool result = base::Time::FromString(str, &start_of_month);
DCHECK(result);
return start_of_month.UTCMidnight();
}
std::unique_ptr<CalendarEventFetch> PerformFetch(
const base::Time start_of_month) {
std::unique_ptr<CalendarEventFetch> fetch =
std::make_unique<CalendarEventFetch>(
start_of_month,
base::BindRepeating(&CalendarEventFetchTest::OnEventsFetched,
base::Unretained(this)),
base::BindRepeating(
&CalendarEventFetchTest::OnEventFetchFailedInternalError,
base::Unretained(this)),
task_environment()->GetMockTickClock());
return fetch;
}
std::unique_ptr<CalendarEventFetch> PerformFetchByCalendarId(
const base::Time start_of_month,
const std::string calendar_id,
const std::string calendar_color_id) {
std::unique_ptr<CalendarEventFetch> fetch =
std::make_unique<CalendarEventFetch>(
start_of_month,
base::BindRepeating(&CalendarEventFetchTest::OnEventsFetched,
base::Unretained(this)),
base::BindRepeating(
&CalendarEventFetchTest::OnEventFetchFailedInternalError,
base::Unretained(this)),
task_environment()->GetMockTickClock(), calendar_id,
calendar_color_id);
return fetch;
}
std::optional<std::string> get_calendar_id() { return calendar_id_; }
std::optional<int> events_fetched_count() { return events_fetched_count_; }
std::optional<google_apis::ApiErrorCode> api_error_code() {
return api_error_code_;
}
std::optional<CalendarEventFetchInternalErrorCode> internal_error_code() {
return internal_error_code_;
}
protected:
TestCalendarClient client_;
private:
const AccountId GetDefaultUserId() {
return AccountId::FromUserEmail("user0@tray");
}
std::optional<std::string> calendar_id_;
std::optional<int> events_fetched_count_;
std::optional<google_apis::ApiErrorCode> api_error_code_;
std::optional<CalendarEventFetchInternalErrorCode> internal_error_code_;
base::WeakPtrFactory<CalendarEventFetchTest> weak_factory_{this};
};
TEST_F(CalendarEventFetchTest, NoEvents) {
// Register our TestCalendarClient with the default user.
RegisterClient();
// Month whose events we want to fetch.
base::Time start_of_month =
GetStartOfMonthFromString("23 Oct 2009 11:30 GMT");
// Perform the fetch.
std::unique_ptr<CalendarEventFetch> fetch = PerformFetch(start_of_month);
// Advance time to when the fetch is complete. `fetch` can no longer be used
// after this.
task_environment()->FastForwardBy(client_.get_response_delay());
// No events were set in the client, so fetch should return no results.
std::optional<int> count = events_fetched_count();
EXPECT_TRUE(count.has_value() && count.value() == 0);
// API error is HTTP_SUCCESS.
std::optional<google_apis::ApiErrorCode> return_error_code = api_error_code();
EXPECT_TRUE(return_error_code.has_value() &&
return_error_code == google_apis::HTTP_SUCCESS);
// No internal error.
EXPECT_FALSE(internal_error_code().has_value());
}
TEST_F(CalendarEventFetchTest, HaveEvents) {
// Register our TestCalendarClient with the default user.
RegisterClient();
// Inject two events, in different months.
auto event_list = std::make_unique<google_apis::calendar::EventList>();
event_list->set_time_zone("Greenwich Mean Time");
event_list->InjectItemForTesting(calendar_test_utils::CreateEvent(
"id_0", "summary_0", "23 Oct 2009 11:30 GMT", "23 Oct 2009 12:30 GMT"));
event_list->InjectItemForTesting(calendar_test_utils::CreateEvent(
"id_1", "summary_1", "18 Nov 2021 8:15 GMT", "18 Nov 2021 11:30 GMT"));
client_.set_event_list(std::move(event_list));
// Month whose events we want to fetch, for which no events have been
// injected.
base::Time start_of_month =
GetStartOfMonthFromString("27 Sep 1971 11:30 GMT");
// Perform the fetch.
std::unique_ptr<CalendarEventFetch> fetch = PerformFetch(start_of_month);
// Advance time to when the fetch is complete. `fetch` can no longer be used
// after this.
task_environment()->FastForwardBy(client_.get_response_delay());
// No events for this month in the client, so fetch should return no results.
std::optional<int> count = events_fetched_count();
EXPECT_TRUE(count.has_value() && count.value() == 0);
// No internal error.
EXPECT_FALSE(internal_error_code().has_value());
// Now use a month that has events
start_of_month = GetStartOfMonthFromString("23 Oct 2009 11:30 GMT");
// Perform the fetch.
std::unique_ptr<CalendarEventFetch> fetch2 = PerformFetch(start_of_month);
// Advance time to when the fetch is complete. `fetch2` can no longer be
// used after this.
task_environment()->FastForwardBy(client_.get_response_delay());
// There is one event for this month in the client, so fetch should return one
// event.
count = events_fetched_count();
EXPECT_TRUE(count.has_value() && count.value() == 1);
// API error is HTTP_SUCCESS.
std::optional<google_apis::ApiErrorCode> return_error_code = api_error_code();
EXPECT_TRUE(return_error_code.has_value() &&
return_error_code == google_apis::HTTP_SUCCESS);
// No internal error.
EXPECT_FALSE(internal_error_code().has_value());
}
TEST_F(CalendarEventFetchTest, FetchEventsForNonPrimaryCalendar) {
RegisterClient();
// The month for which we want to fetch events.
base::Time start_of_month = GetStartOfMonthFromString("01 Oct 2023 8:00 GMT");
// The ID and color ID of the calendar we want to fetch events for.
std::string calendar_id = "[email protected]";
std::string calendar_color_id = "14";
// Inject some events.
auto event_list = std::make_unique<google_apis::calendar::EventList>();
event_list->set_time_zone("Greenwich Mean Time");
event_list->InjectItemForTesting(calendar_test_utils::CreateEvent(
"id_0", "summary_0", "02 Oct 2023 17:00 GMT", "02 Oct 2023 18:00 GMT"));
event_list->InjectItemForTesting(calendar_test_utils::CreateEvent(
"id_1", "summary_1", "05 Oct 2023 19:00 GMT", "05 Oct 2023 20:00 GMT"));
client_.set_event_list(std::move(event_list));
std::unique_ptr<CalendarEventFetch> fetch =
PerformFetchByCalendarId(start_of_month, calendar_id, calendar_color_id);
// Advance time to when the fetch is complete. `fetch` can no longer be used
// after this.
task_environment()->FastForwardBy(client_.get_response_delay());
// There are two events for this month in the client, so fetch should return
// two events.
std::optional<int> count = events_fetched_count();
EXPECT_TRUE(count.has_value() && count.value() == 2);
// API error is HTTP_SUCCESS.
std::optional<google_apis::ApiErrorCode> return_error_code = api_error_code();
EXPECT_TRUE(return_error_code.has_value() &&
return_error_code == google_apis::HTTP_SUCCESS);
// No internal error.
EXPECT_FALSE(internal_error_code().has_value());
// The calendar ID assigned on fetch completion should equal the calendar ID
// passed during the creation of the fetch.
std::optional<std::string> fetch_calendar_id = get_calendar_id();
EXPECT_TRUE(fetch_calendar_id.has_value() &&
fetch_calendar_id == calendar_id);
}
TEST_F(CalendarEventFetchTest, ApiFailure) {
// Specifically set up the fetch to fail with an API error.
const google_apis::ApiErrorCode error_code = google_apis::HTTP_NOT_FOUND;
client_.set_api_error_code(error_code);
// Register our TestCalendarClient with the default user.
RegisterClient();
// Month whose events we want to fetch.
base::Time start_of_month =
GetStartOfMonthFromString("23 Oct 2009 11:30 GMT");
// Perform the fetch.
std::unique_ptr<CalendarEventFetch> fetch = PerformFetch(start_of_month);
// Advance time to when the fetch is complete. `fetch` can no longer be used
// after this.
task_environment()->FastForwardBy(client_.get_response_delay());
// No events were set in the client, so fetch should return no results.
std::optional<int> count = events_fetched_count();
EXPECT_TRUE(count.has_value() && count.value() == 0);
// API error is what we set.
std::optional<google_apis::ApiErrorCode> return_error_code = api_error_code();
EXPECT_TRUE(return_error_code.has_value() && return_error_code == error_code);
// No internal error.
EXPECT_FALSE(internal_error_code().has_value());
}
TEST_F(CalendarEventFetchTest, Timeout) {
base::HistogramTester histogram_tester;
// No metrics recorded yet.
histogram_tester.ExpectBucketCount("Ash.Calendar.FetchEvents.Timeout", true,
/*expected_count=*/0);
// Specifically delay the response until after CalendarEventFetch declares a
// timeout.
client_.set_response_delay(calendar_utils::kCalendarDataFetchTimeout +
base::Milliseconds(100));
// Register our TestCalendarClient with the default user.
RegisterClient();
// Month whose events we want to fetch.
base::Time start_of_month =
GetStartOfMonthFromString("23 Oct 2009 11:30 GMT");
// No internal error code reported.
std::optional<CalendarEventFetchInternalErrorCode> internal_error =
internal_error_code();
EXPECT_FALSE(internal_error.has_value());
// Perform the fetch.
std::unique_ptr<CalendarEventFetch> fetch = PerformFetch(start_of_month);
// Advance time to when the fetch times out. `fetch` can no longer be used
// after this.
task_environment()->FastForwardBy(calendar_utils::kCalendarDataFetchTimeout);
// Events should be completely nonexistent.
EXPECT_FALSE(events_fetched_count().has_value());
// API error should be completely nonexistent.
std::optional<google_apis::ApiErrorCode> return_error_code = api_error_code();
EXPECT_FALSE(return_error_code.has_value());
// Internal error code reported is kTimeout.
internal_error = internal_error_code();
EXPECT_TRUE(internal_error.has_value() &&
internal_error == CalendarEventFetchInternalErrorCode::kTimeout);
// Metrics now recorded.
histogram_tester.ExpectBucketCount("Ash.Calendar.FetchEvents.Timeout", true,
/*expected_count=*/1);
}
TEST_F(CalendarEventFetchTest, Cancel) {
// Register our TestCalendarClient with the default user.
RegisterClient();
// Month whose events we want to fetch.
base::Time start_of_month =
GetStartOfMonthFromString("23 Oct 2009 11:30 GMT");
// Perform the fetch.
std::unique_ptr<CalendarEventFetch> fetch = PerformFetch(start_of_month);
// Cancel the request.
fetch->Cancel();
// Advance time to when the fetch is complete. `fetch` can no longer be used
// after this.
task_environment()->FastForwardBy(client_.get_response_delay());
// API error is CANCELLED.
std::optional<google_apis::ApiErrorCode> return_error_code = api_error_code();
EXPECT_TRUE(return_error_code.has_value() &&
return_error_code == google_apis::CANCELLED);
}
} // namespace ash