chromium/chromeos/ash/components/drivefs/drivefs_auth_unittest.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/drivefs/drivefs_auth.h"

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/test/bind.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/timer/mock_timer.h"
#include "components/account_id/account_id.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace drivefs {
namespace {

using testing::_;

constexpr char kTestEmail[] = "[email protected]";
constexpr base::TimeDelta kTokenLifetime = base::Hours(1);

class AuthDelegateImpl : public DriveFsAuth::Delegate {
 public:
  AuthDelegateImpl(signin::IdentityManager* identity_manager,
                   const AccountId& account_id)
      : identity_manager_(identity_manager), account_id_(account_id) {}

  AuthDelegateImpl(const AuthDelegateImpl&) = delete;
  AuthDelegateImpl& operator=(const AuthDelegateImpl&) = delete;

  ~AuthDelegateImpl() override = default;

 private:
  // AuthDelegate::Delegate:
  scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory()
      override {
    return nullptr;
  }
  signin::IdentityManager* GetIdentityManager() override {
    return identity_manager_;
  }
  const AccountId& GetAccountId() override { return account_id_; }
  std::string GetObfuscatedAccountId() override {
    return "salt-" + account_id_.GetAccountIdKey();
  }

  bool IsMetricsCollectionEnabled() override { return false; }

  const raw_ptr<signin::IdentityManager> identity_manager_;
  const AccountId account_id_;
};

class DriveFsAuthTest : public ::testing::Test {
 public:
  DriveFsAuthTest() = default;

  DriveFsAuthTest(const DriveFsAuthTest&) = delete;
  DriveFsAuthTest& operator=(const DriveFsAuthTest&) = delete;

 protected:
  void SetUp() override {
    clock_.SetNow(base::Time::Now());
    identity_test_env_.MakePrimaryAccountAvailable(
        kTestEmail, signin::ConsentLevel::kSignin);
    auto timer = std::make_unique<base::MockOneShotTimer>();
    timer_ = timer.get();
    delegate_ = std::make_unique<AuthDelegateImpl>(
        identity_test_env_.identity_manager(),
        AccountId::FromUserEmailGaiaId(kTestEmail, "ID"));
    auth_ = std::make_unique<DriveFsAuth>(&clock_,
                                          base::FilePath("/path/to/profile"),
                                          std::move(timer), delegate_.get());
  }

  void TearDown() override {
    EXPECT_FALSE(timer_->IsRunning());
    auth_.reset();
  }

  // Helper function for better line wrapping.
  void RespondWithAccessToken(const std::string& token) {
    identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
        token, clock_.Now() + kTokenLifetime);
  }

  // Helper function for better line wrapping.
  void RespondWithAuthError(GoogleServiceAuthError::State error_state) {
    identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
        GoogleServiceAuthError(error_state));
  }

  base::test::TaskEnvironment task_environment_;
  signin::IdentityTestEnvironment identity_test_env_;
  base::SimpleTestClock clock_;

  std::unique_ptr<AuthDelegateImpl> delegate_;
  std::unique_ptr<DriveFsAuth> auth_;
  raw_ptr<base::MockOneShotTimer, DanglingUntriaged> timer_ = nullptr;
};

TEST_F(DriveFsAuthTest, GetAccessToken_Success) {
  const base::Time time_now = base::Time::Now();
  clock_.SetNow(time_now);
  base::RunLoop run_loop;
  auth_->GetAccessToken(
      false,
      base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                     mojom::AccessTokenPtr access_token) {
        EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
        EXPECT_EQ("auth token", access_token->token);
        EXPECT_EQ(time_now + kTokenLifetime, access_token->expiry_time);
        run_loop.Quit();
      }));
  RespondWithAccessToken("auth token");
  run_loop.Run();
}

TEST_F(DriveFsAuthTest, GetAccessToken_GetAccessTokenFailure_Permanent) {
  base::RunLoop run_loop;
  auth_->GetAccessToken(
      false,
      base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                     mojom::AccessTokenPtr access_token) {
        EXPECT_EQ(mojom::AccessTokenStatus::kAuthError, status);
        EXPECT_FALSE(access_token.is_null());
        run_loop.Quit();
      }));
  RespondWithAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
  run_loop.Run();
}

TEST_F(DriveFsAuthTest, GetAccessToken_GetAccessTokenFailure_Transient) {
  base::RunLoop run_loop;
  auth_->GetAccessToken(
      false,
      base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                     mojom::AccessTokenPtr access_token) {
        EXPECT_EQ(mojom::AccessTokenStatus::kTransientError, status);
        EXPECT_FALSE(access_token.is_null());
        run_loop.Quit();
      }));
  RespondWithAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE);
  run_loop.Run();
}

TEST_F(DriveFsAuthTest, GetAccessToken_GetAccessTokenFailure_Timeout) {
  base::RunLoop run_loop;
  auto quit_closure = run_loop.QuitClosure();
  auth_->GetAccessToken(
      false, base::BindLambdaForTesting(
                 [&](mojom::AccessTokenStatus status, mojom::AccessTokenPtr) {
                   EXPECT_EQ(mojom::AccessTokenStatus::kTransientError, status);
                   std::move(quit_closure).Run();
                 }));
  // Timer fires before access token becomes available.
  timer_->Fire();
  run_loop.Run();
}

TEST_F(DriveFsAuthTest, GetAccessToken_GetAccessTokenFailure_TimeoutRace) {
  base::RunLoop run_loop;
  auto quit_closure = run_loop.QuitClosure();
  auth_->GetAccessToken(
      false, base::BindLambdaForTesting(
                 [&](mojom::AccessTokenStatus status, mojom::AccessTokenPtr) {
                   EXPECT_EQ(mojom::AccessTokenStatus::kTransientError, status);
                   std::move(quit_closure).Run();
                 }));
  // Timer fires before access token becomes available.
  timer_->Fire();
  // Timer callback should stop access token retrieval.
  RespondWithAccessToken("auth token");
  run_loop.Run();
}

TEST_F(DriveFsAuthTest, GetAccessToken_ParallelRequests) {
  const base::Time time_now = base::Time::Now();
  clock_.SetNow(time_now);
  base::RunLoop run_loop;
  auto quit_closure = run_loop.QuitClosure();
  auth_->GetAccessToken(
      false,
      base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                     mojom::AccessTokenPtr access_token) {
        EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
        EXPECT_EQ("auth token", access_token->token);
        EXPECT_EQ(time_now + kTokenLifetime, access_token->expiry_time);
        std::move(quit_closure).Run();
      }));
  auth_->GetAccessToken(
      false,
      base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                     mojom::AccessTokenPtr access_token) {
        EXPECT_EQ(mojom::AccessTokenStatus::kTransientError, status);
        EXPECT_FALSE(access_token.is_null());
      }));
  RespondWithAccessToken("auth token");
  run_loop.Run();
}

TEST_F(DriveFsAuthTest, GetAccessToken_SequentialRequests) {
  for (int i = 0; i < 3; ++i) {
    base::RunLoop run_loop;
    auth_->GetAccessToken(
        false,
        base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                       mojom::AccessTokenPtr access_token) {
          EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
          EXPECT_EQ("auth token", access_token->token);
          run_loop.Quit();
        }));
    RespondWithAccessToken("auth token");
    run_loop.Run();
  }
  for (int i = 0; i < 3; ++i) {
    base::RunLoop run_loop;
    auth_->GetAccessToken(
        false,
        base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                       mojom::AccessTokenPtr access_token) {
          EXPECT_EQ(mojom::AccessTokenStatus::kAuthError, status);
          EXPECT_FALSE(access_token.is_null());
          run_loop.Quit();
        }));
    RespondWithAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
    run_loop.Run();
  }
}

TEST_F(DriveFsAuthTest, Caching) {
  const base::Time time_now = base::Time::Now();
  clock_.SetNow(time_now);
  const base::Time expiry_time = time_now + kTokenLifetime;
  auth_->GetAccessToken(
      true, base::BindLambdaForTesting(
                [&expiry_time](mojom::AccessTokenStatus status,
                               mojom::AccessTokenPtr access_token) {
                  EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
                  EXPECT_EQ("auth token", access_token->token);
                  EXPECT_EQ(expiry_time, access_token->expiry_time);
                }));
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());
  RespondWithAccessToken("auth token");

  // Second attempt should reuse already available token.
  auth_->GetAccessToken(
      true, base::BindLambdaForTesting(
                [&expiry_time](mojom::AccessTokenStatus status,
                               mojom::AccessTokenPtr access_token) {
                  EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
                  EXPECT_EQ("auth token", access_token->token);
                  EXPECT_EQ(expiry_time, access_token->expiry_time);
                }));
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
}

TEST_F(DriveFsAuthTest, CachedAndNotCached) {
  base::Time time_now = base::Time::Now();
  clock_.SetNow(time_now);
  const base::Time expiry_time = time_now + kTokenLifetime;
  auth_->GetAccessToken(
      true, base::BindLambdaForTesting(
                [&expiry_time](mojom::AccessTokenStatus status,
                               mojom::AccessTokenPtr access_token) {
                  EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
                  EXPECT_EQ("auth token", access_token->token);
                  EXPECT_EQ(expiry_time, access_token->expiry_time);
                }));
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());
  RespondWithAccessToken("auth token");

  // Advance the clock 30 mins, the token should still retain the old expiry
  // time.
  clock_.Advance(base::Minutes(30));
  time_now = time_now + base::Minutes(30);

  // Second attempt should reuse already available token.
  auth_->GetAccessToken(
      true, base::BindLambdaForTesting(
                [&expiry_time](mojom::AccessTokenStatus status,
                               mojom::AccessTokenPtr access_token) {
                  EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
                  EXPECT_EQ("auth token", access_token->token);
                  EXPECT_EQ(expiry_time, access_token->expiry_time);
                }));
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());

  // Now ask for token explicitly bypassing the cache.
  auth_->GetAccessToken(
      false, base::BindLambdaForTesting(
                 [&time_now](mojom::AccessTokenStatus status,
                             mojom::AccessTokenPtr access_token) {
                   EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
                   EXPECT_EQ("auth token 2", access_token->token);
                   EXPECT_EQ(time_now + kTokenLifetime,
                             access_token->expiry_time);
                 }));
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());
  RespondWithAccessToken("auth token 2");
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
}

TEST_F(DriveFsAuthTest, CacheExpired) {
  auth_->GetAccessToken(
      true, base::BindLambdaForTesting([](mojom::AccessTokenStatus status,
                                          mojom::AccessTokenPtr access_token) {
        EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
        EXPECT_EQ("auth token", access_token->token);
      }));
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());
  RespondWithAccessToken("auth token");

  clock_.Advance(base::Hours(2));

  // The token expired so a new one is requested.
  auth_->GetAccessToken(
      true, base::BindLambdaForTesting([](mojom::AccessTokenStatus status,
                                          mojom::AccessTokenPtr access_token) {
        EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
        EXPECT_EQ("auth token 2", access_token->token);
      }));
  RespondWithAccessToken("auth token 2");
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
}

}  // namespace
}  // namespace drivefs