chromium/ash/system/time/calendar_list_model_unittest.cc

// Copyright 2024 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_list_model.h"

#include <iterator>
#include <list>
#include <memory>
#include <string>
#include <vector>

#include "ash/calendar/calendar_client.h"
#include "ash/calendar/calendar_controller.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/session/session_controller.h"
#include "ash/public/cpp/session/session_types.h"
#include "ash/public/cpp/session/user_info.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/time/calendar_unittest_utils.h"
#include "ash/system/time/calendar_utils.h"
#include "ash/test/ash_test_base.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/session_manager/session_manager_types.h"
#include "google_apis/calendar/calendar_api_response_types.h"
#include "google_apis/common/api_error_codes.h"

namespace ash {

namespace {

using ::google_apis::calendar::SingleCalendar;

constexpr char kId1[] = "[email protected]";
constexpr char kSummary1[] = "A-Team OOO";
constexpr char kColorId1[] = "c7";
bool kSelected1 = true;
bool kPrimary1 = false;

constexpr char kId2[] = "[email protected]";
constexpr char kSummary2[] = "[email protected]";
constexpr char kColorId2[] = "c12";
bool kSelected2 = true;
bool kPrimary2 = true;

constexpr char kId3[] = "[email protected]";
constexpr char kSummary3[] = "Birthdays";
constexpr char kColorId3[] = "c14";
bool kSelected3 = true;
bool kPrimary3 = false;

constexpr char kId4[] = "[email protected]";
constexpr char kSummary4[] = "NYC Food Pop Ups";
constexpr char kColorId4[] = "c3";
bool kSelected4 = true;
bool kPrimary4 = false;

constexpr char kId5[] = "[email protected]";
constexpr char kSummary5[] = "Happy Hour Events";
constexpr char kColorId5[] = "c5";
bool kSelected5 = true;
bool kPrimary5 = false;

constexpr char kId6[] = "[email protected]";
constexpr char kSummary6[] = "On-call Rotation";
constexpr char kColorId6[] = "c12";
bool kSelected6 = true;
bool kPrimary6 = false;

constexpr char kId7[] = "[email protected]";
constexpr char kSummary7[] = "Running Club";
constexpr char kColorId7[] = "c11";
bool kSelected7 = true;
bool kPrimary7 = false;

constexpr char kId8[] = "[email protected]";
constexpr char kSummary8[] = "Company holidays";
constexpr char kColorId8[] = "c9";
bool kSelected8 = true;
bool kPrimary8 = false;

constexpr char kId9[] = "[email protected]";
constexpr char kSummary9[] = "Soccer Games";
constexpr char kColorId9[] = "c10";
bool kSelected9 = true;
bool kPrimary9 = false;

constexpr char kId10[] = "[email protected]";
constexpr char kSummary10[] = "Writing Club";
constexpr char kColorId10[] = "c1";
bool kSelected10 = true;
bool kPrimary10 = false;

constexpr char kId11[] = "[email protected]";
constexpr char kSummary11[] = "Family";
constexpr char kColorId11[] = "c5";
bool kSelected11 = true;
bool kPrimary11 = false;

constexpr char kId12[] = "[email protected]";
constexpr char kSummary12[] = "Band Rehearsal";
constexpr char kColorId12[] = "c7";
bool kSelected12 = false;
bool kPrimary12 = false;

std::unique_ptr<google_apis::calendar::CalendarList> CreateMockCalendarList() {
  std::list<std::unique_ptr<google_apis::calendar::SingleCalendar>> calendars;
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId1, kSummary1, kColorId1, kSelected1, kPrimary1));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId2, kSummary2, kColorId2, kSelected2, kPrimary2));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId3, kSummary3, kColorId3, kSelected3, kPrimary3));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId12, kSummary12, kColorId12, kSelected12, kPrimary12));
  return calendar_test_utils::CreateMockCalendarList(std::move(calendars));
}

}  // namespace

class CalendarListModelTest : public AshTestBase {
 public:
  CalendarListModelTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

  CalendarListModelTest(const CalendarListModelTest& other) = delete;
  CalendarListModelTest& operator=(const CalendarListModelTest& other) = delete;
  ~CalendarListModelTest() override = default;

  void SetUp() override {
    // Enable the Multi-Calendar feature.
    scoped_feature_list_.InitAndEnableFeature(
        ash::features::kMultiCalendarSupport);

    AshTestBase::SetUp();

    // Register a mock `CalendarClient` to the `CalendarController`.
    const std::string email = "[email protected]";
    AccountId account_id = AccountId::FromUserEmail(email);
    Shell::Get()->calendar_controller()->SetActiveUserAccountIdForTesting(
        account_id);
    calendar_list_model_ = std::make_unique<CalendarListModel>();
    calendar_client_ =
        std::make_unique<calendar_test_utils::CalendarClientTestImpl>();
    Shell::Get()->calendar_controller()->RegisterClientForUser(
        account_id, calendar_client_.get());
    Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
        ash::prefs::kCalendarIntegrationEnabled, true);
  }

  void TearDown() override {
    calendar_list_model_.reset();
    scoped_feature_list_.Reset();
    AshTestBase::TearDown();
  }

  // Wait until the response is back. Since we used `PostDelayedTask` with 1
  // second to mimic the behavior of fetching, duration of 1 minute should be
  // enough.
  void WaitUntilFetched() {
    task_environment()->FastForwardBy(base::Minutes(1));
    base::RunLoop().RunUntilIdle();
  }

  void UpdateSession(uint32_t session_id,
                     const std::string& email,
                     bool is_child = false) {
    UserSession session;
    session.session_id = session_id;
    session.user_info.type = is_child ? user_manager::UserType::kChild
                                      : user_manager::UserType::kRegular;
    session.user_info.account_id = AccountId::FromUserEmail(email);
    session.user_info.display_name = email;
    session.user_info.display_email = email;
    session.user_info.is_new_profile = false;

    SessionController::Get()->UpdateUserSession(session);
  }

  CalendarListModel* calendar_list_model() {
    return calendar_list_model_.get();
  }

  calendar_test_utils::CalendarClientTestImpl* client() {
    return calendar_client_.get();
  }

  std::unique_ptr<CalendarListModel> calendar_list_model_;
  std::unique_ptr<calendar_test_utils::CalendarClientTestImpl> calendar_client_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_F(CalendarListModelTest, FetchShortCalendarList) {
  // Set up list of calendars as the mock response.
  client()->SetCalendarList(CreateMockCalendarList());

  // Fetching should not be in progress and the calendar list should not be
  // cached.
  EXPECT_FALSE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_FALSE(calendar_list_model()->get_is_cached());

  // Fetch the calendars. The model should report that a fetch is in progress
  // and a calendar list is not yet cached.
  calendar_list_model()->FetchCalendars();
  EXPECT_TRUE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_FALSE(calendar_list_model()->get_is_cached());

  WaitUntilFetched();

  // The model should now report that a fetch is not in progress and a calendar
  // list is cached.
  EXPECT_FALSE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_TRUE(calendar_list_model()->get_is_cached());

  CalendarList calendar_list = calendar_list_model()->GetCachedCalendarList();

  // Verify that the length of the result matches the number of calendars in
  // the mock list that are selected.
  EXPECT_EQ(3u, calendar_list.size());

  // Verify that the next calendars in the result are sorted in alphabetical
  // order.
  ash::CalendarList::iterator calendar_2_it = std::next(calendar_list.begin());
  EXPECT_EQ(calendar_2_it->summary(), kSummary1);
  ash::CalendarList::iterator calendar_3_it = std::next(calendar_2_it);
  EXPECT_EQ(calendar_3_it->summary(), kSummary3);

  // Set up list of calendars as the mock response.
  client()->SetCalendarList(CreateMockCalendarList());

  // Trigger a refetch. The model should report that a fetch is in progress and
  // a calendar list is already cached.
  calendar_list_model()->FetchCalendars();
  EXPECT_TRUE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_TRUE(calendar_list_model()->get_is_cached());

  WaitUntilFetched();

  // The model should now report that a fetch is not in progress and a calendar
  // list is cached. The size of the result should be as expected.
  EXPECT_FALSE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_TRUE(calendar_list_model()->get_is_cached());
  calendar_list = calendar_list_model()->GetCachedCalendarList();
  EXPECT_EQ(3u, calendar_list.size());
}

TEST_F(CalendarListModelTest, FetchLongCalendarList) {
  // Set up list of calendars as the mock response.
  std::list<std::unique_ptr<google_apis::calendar::SingleCalendar>> calendars;
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId1, kSummary1, kColorId1, kSelected1, kPrimary1));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId2, kSummary2, kColorId2, kSelected2, kPrimary2));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId3, kSummary3, kColorId3, kSelected3, kPrimary3));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId4, kSummary4, kColorId4, kSelected4, kPrimary4));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId5, kSummary5, kColorId5, kSelected5, kPrimary5));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId6, kSummary6, kColorId6, kSelected6, kPrimary6));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId7, kSummary7, kColorId7, kSelected7, kPrimary7));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId8, kSummary8, kColorId8, kSelected8, kPrimary8));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId9, kSummary9, kColorId9, kSelected9, kPrimary9));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId10, kSummary10, kColorId10, kSelected10, kPrimary10));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId11, kSummary11, kColorId11, kSelected11, kPrimary11));
  calendars.push_back(calendar_test_utils::CreateCalendar(
      kId12, kSummary12, kColorId12, kSelected12, kPrimary12));
  client()->SetCalendarList(
      calendar_test_utils::CreateMockCalendarList(std::move(calendars)));

  // Fetching should not be in progress and the calendar list should not be
  // cached.
  EXPECT_FALSE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_FALSE(calendar_list_model()->get_is_cached());

  // Fetch the calendars. The model should report that a fetch is in progress
  // and a calendar list is not yet cached.
  calendar_list_model()->FetchCalendars();
  EXPECT_TRUE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_FALSE(calendar_list_model()->get_is_cached());

  WaitUntilFetched();

  // The model should now report that a fetch is not in progress and a calendar
  // list is cached.
  EXPECT_FALSE(calendar_list_model()->get_fetch_in_progress());
  EXPECT_TRUE(calendar_list_model()->get_is_cached());

  CalendarList calendar_list = calendar_list_model()->GetCachedCalendarList();

  // Verify that the length of the result equals `kMultipleCalendarsLimit`.
  EXPECT_EQ(10u, calendar_list.size());

  // Verify that the primary calendar is the first entry in the result.
  EXPECT_TRUE(calendar_list.front().primary());

  // Verify that the next calendars in the result are sorted in the expected
  // order (alphabetically, with the unselected calendar absent).
  ash::CalendarList::iterator calendar_2_it = std::next(calendar_list.begin());
  EXPECT_EQ(calendar_2_it->summary(), kSummary1);
  ash::CalendarList::iterator calendar_3_it = std::next(calendar_2_it);
  EXPECT_EQ(calendar_3_it->summary(), kSummary3);
  ash::CalendarList::iterator calendar_4_it = std::next(calendar_3_it);
  EXPECT_EQ(calendar_4_it->summary(), kSummary8);
  ash::CalendarList::iterator calendar_5_it = std::next(calendar_4_it);
  EXPECT_EQ(calendar_5_it->summary(), kSummary11);
  ash::CalendarList::iterator calendar_6_it = std::next(calendar_5_it);
  EXPECT_EQ(calendar_6_it->summary(), kSummary5);
  ash::CalendarList::iterator calendar_7_it = std::next(calendar_6_it);
  EXPECT_EQ(calendar_7_it->summary(), kSummary4);
  ash::CalendarList::iterator calendar_8_it = std::next(calendar_7_it);
  EXPECT_EQ(calendar_8_it->summary(), kSummary6);
  ash::CalendarList::iterator calendar_9_it = std::next(calendar_8_it);
  EXPECT_EQ(calendar_9_it->summary(), kSummary7);
  ash::CalendarList::iterator calendar_10_it = std::next(calendar_9_it);
  EXPECT_EQ(calendar_10_it->summary(), kSummary9);
}

TEST_F(CalendarListModelTest, ActiveUserChange) {
  // Set up two users, user1 is the active user.
  UpdateSession(1u, "[email protected]");
  UpdateSession(2u, "[email protected]");
  std::vector<uint32_t> order = {1u, 2u};
  SessionController::Get()->SetUserSessionOrder(order);
  base::RunLoop().RunUntilIdle();

  // Set up list of calendars as the mock response.
  client()->SetCalendarList(CreateMockCalendarList());

  calendar_list_model()->FetchCalendars();
  WaitUntilFetched();

  // Verify that the length of the result matches the number of calendars in
  // the mock list that are selected.
  EXPECT_TRUE(calendar_list_model()->get_is_cached());
  CalendarList calendar_list = calendar_list_model()->GetCachedCalendarList();
  EXPECT_EQ(3u, calendar_list.size());

  // Make user2 the active user, and the cached calendars should be cleared.
  order = {2u, 1u};
  SessionController::Get()->SetUserSessionOrder(order);
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(calendar_list_model()->get_is_cached());
  calendar_list = calendar_list_model()->GetCachedCalendarList();
  EXPECT_EQ(0u, calendar_list.size());
}

TEST_F(CalendarListModelTest, ActiveChildUserChange) {
  // Set up two child users, user1 is the active user.
  UpdateSession(1u, "[email protected]", /*is_child=*/true);
  UpdateSession(2u, "[email protected]", /*is_child=*/true);
  std::vector<uint32_t> order = {1u, 2u};
  SessionController::Get()->SetUserSessionOrder(order);
  base::RunLoop().RunUntilIdle();

  // Set up list of calendars as the mock response.
  client()->SetCalendarList(CreateMockCalendarList());

  calendar_list_model()->FetchCalendars();
  WaitUntilFetched();

  // Verify that the length of the result matches the number of calendars in
  // the mock list that are selected.
  EXPECT_TRUE(calendar_list_model()->get_is_cached());
  CalendarList calendar_list = calendar_list_model()->GetCachedCalendarList();
  EXPECT_EQ(3u, calendar_list.size());

  // Make user2 the active user, and the cached calendars should be cleared.
  order = {2u, 1u};
  SessionController::Get()->SetUserSessionOrder(order);
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(calendar_list_model()->get_is_cached());
  calendar_list = calendar_list_model()->GetCachedCalendarList();
  EXPECT_EQ(0u, calendar_list.size());
}

TEST_F(CalendarListModelTest, ClearCalendars) {
  // Set up list of calendars as the mock response.
  client()->SetCalendarList(CreateMockCalendarList());

  calendar_list_model()->FetchCalendars();
  WaitUntilFetched();

  // Verify that the length of the result matches the number of calendars in
  // the mock list that are selected.
  EXPECT_TRUE(calendar_list_model()->get_is_cached());
  CalendarList calendar_list = calendar_list_model()->GetCachedCalendarList();
  EXPECT_EQ(3u, calendar_list.size());

  // Simulate a session change to clear the calendar list.
  calendar_list_model()->OnSessionStateChanged(
      session_manager::SessionState::LOCKED);

  // Verify that the list is empty after clearing the list and the model
  // indicates that there is no calendar list cached.
  EXPECT_FALSE(calendar_list_model()->get_is_cached());
  calendar_list = calendar_list_model()->GetCachedCalendarList();
  EXPECT_EQ(0u, calendar_list.size());
}

TEST_F(CalendarListModelTest, RecordFetchResultHistogram_Success) {
  base::HistogramTester histogram_tester;

  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Result",
                                     google_apis::HTTP_SUCCESS,
                                     /*expected_count=*/0);

  calendar_list_model()->FetchCalendars();

  WaitUntilFetched();

  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Result",
                                     google_apis::HTTP_SUCCESS,
                                     /*expected_count=*/1);
}

TEST_F(CalendarListModelTest, RecordFetchResultHistogram_Failure) {
  base::HistogramTester histogram_tester;

  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Result",
                                     google_apis::HTTP_SUCCESS,
                                     /*expected_count=*/0);

  client()->SetError(google_apis::NO_CONNECTION);
  calendar_list_model()->FetchCalendars();

  WaitUntilFetched();

  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Result",
                                     google_apis::NO_CONNECTION,
                                     /*expected_count=*/1);

  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Result",
                                     google_apis::HTTP_SUCCESS,
                                     /*expected_count=*/0);
}

TEST_F(CalendarListModelTest, RecordFetchResultHistogram_Cancelled) {
  base::HistogramTester histogram_tester;

  // Set mock calendar list and error code in the client.
  client()->SetCalendarList(CreateMockCalendarList());
  client()->SetError(google_apis::CANCELLED);
  calendar_list_model()->FetchCalendars();

  calendar_list_model()->CancelFetch();

  WaitUntilFetched();

  // There should be no calendar list cached despite a calendar list being set
  // in the client.
  EXPECT_FALSE(calendar_list_model()->get_is_cached());

  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Result",
                                     google_apis::CANCELLED,
                                     /*expected_count=*/1);

  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Result",
                                     google_apis::HTTP_SUCCESS,
                                     /*expected_count=*/0);
}

TEST_F(CalendarListModelTest, RecordFetchTimeout) {
  base::HistogramTester histogram_tester;

  // No timeout has been recorded yet.
  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Timeout",
                                     true,
                                     /*expected_count=*/0);

  client()->SetCalendarList(CreateMockCalendarList());

  // Delay the response until after the model declares a timeout.
  client()->SetResponseDelay(calendar_utils::kCalendarDataFetchTimeout +
                             base::Milliseconds(100));

  calendar_list_model()->FetchCalendars();

  task_environment()->FastForwardBy(calendar_utils::kCalendarDataFetchTimeout);

  // There should be no calendar list cached due to the timeout.
  EXPECT_FALSE(calendar_list_model()->get_is_cached());

  // A timeout should be recorded.
  histogram_tester.ExpectBucketCount("Ash.Calendar.FetchCalendars.Timeout",
                                     true,
                                     /*expected_count=*/1);
}

}  // namespace ash