chromium/chrome/browser/push_notification/server_client/push_notification_server_client_desktop_impl_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 "chrome/browser/push_notification/server_client/push_notification_server_client_desktop_impl.h"

#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/gtest_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/null_task_runner.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/browser/push_notification/metrics/push_notification_metrics.h"
#include "chrome/browser/push_notification/protos/notifications_multi_login_update.pb.h"
#include "chrome/browser/push_notification/server_client/push_notification_desktop_api_call_flow.h"
#include "chrome/browser/push_notification/server_client/push_notification_desktop_api_call_flow_impl.h"
#include "chrome/browser/push_notification/server_client/push_notification_server_client.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace {

const char kGet[] = "GET";
const char kPost[] = "POST";
const char kPatch[] = "PATCH";
const char kAccessToken[] = "access_token";
const char kMultiLoginPath[] = "multiloginupdate";
const char kEmail[] = "[email protected]";
const char kTestGoogleApisUrl[] = "https://notifications-pa.googleapis.com";
const char kRegistrationId[] = "my-device";
const char kApplicationId[] = "com.google.chrome.push_notification";
const char kToken[] = "my-token";
const char kClientId[] = "ChromeDesktop";
const char kOAuthTokenRetrievalResult[] =
    "PushNotification.ChromeOS.OAuth.Token.RetrievalResult";

class FakePushNotificationApiCallFlow
    : public push_notification::PushNotificationDesktopApiCallFlow {
 public:
  FakePushNotificationApiCallFlow() = default;
  ~FakePushNotificationApiCallFlow() override = default;
  FakePushNotificationApiCallFlow(FakePushNotificationApiCallFlow&) = delete;
  FakePushNotificationApiCallFlow& operator=(FakePushNotificationApiCallFlow&) =
      delete;
  void StartPostRequest(
      const GURL& request_url,
      const std::string& serialized_request,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
      const std::string& access_token,
      ResultCallback&& result_callback,
      ErrorCallback&& error_callback) override {
    http_method_ = kPost;
    request_url_ = request_url;
    serialized_request_ = serialized_request;
    url_loader_factory_ = url_loader_factory;
    result_callback_ = std::move(result_callback);
    error_callback_ = std::move(error_callback);
    EXPECT_EQ(kAccessToken, access_token);
  }
  void StartPatchRequest(
      const GURL& request_url,
      const std::string& serialized_request,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
      const std::string& access_token,
      ResultCallback&& result_callback,
      ErrorCallback&& error_callback) override {
    http_method_ = kPatch;
    request_url_ = request_url;
    serialized_request_ = serialized_request;
    url_loader_factory_ = url_loader_factory;
    result_callback_ = std::move(result_callback);
    error_callback_ = std::move(error_callback);
    EXPECT_EQ(kAccessToken, access_token);
  }
  void StartGetRequest(
      const GURL& request_url,
      const QueryParameters& request_as_query_parameters,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
      const std::string& access_token,
      ResultCallback&& result_callback,
      ErrorCallback&& error_callback) override {
    http_method_ = kGet;
    request_url_ = request_url;
    request_as_query_parameters_ = request_as_query_parameters;
    url_loader_factory_ = url_loader_factory;
    result_callback_ = std::move(result_callback);
    error_callback_ = std::move(error_callback);
    EXPECT_EQ(kAccessToken, access_token);
  }
  void SetPartialNetworkTrafficAnnotation(
      const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation)
      override {
    NOTIMPLEMENTED();
  }

  std::string http_method_;
  GURL request_url_;
  std::string serialized_request_;
  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
  ResultCallback result_callback_;
  ErrorCallback error_callback_;
  QueryParameters request_as_query_parameters_;
};

// Callback that should never be invoked.
template <class T>
void NotCalled(T type) {
  EXPECT_TRUE(false);
}
// Callback that should never be invoked.
template <class T>
void NotCalledConstRef(const T& type) {
  EXPECT_TRUE(false);
}

}  // namespace

namespace push_notification {

class PushNotificationServerClientDesktopImplTest : public testing::Test {
 protected:
  PushNotificationServerClientDesktopImplTest() {
    shared_factory_ =
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            base::BindOnce([]() -> network::mojom::URLLoaderFactory* {
              ADD_FAILURE() << "Did not expect this to actually be used";
              return nullptr;
            }));
  }

  void SetUp() override {
    identity_test_environment_.MakePrimaryAccountAvailable(
        kEmail, signin::ConsentLevel::kSync);
    std::unique_ptr<FakePushNotificationApiCallFlow> api_call_flow =
        std::make_unique<FakePushNotificationApiCallFlow>();
    api_call_flow_ = api_call_flow.get();
    client_ = PushNotificationServerClientDesktopImpl::Factory::Create(
        std::move(api_call_flow), identity_test_environment_.identity_manager(),
        shared_factory_);
    histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                        /*bucket: failure=*/0, 0);
    histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                        /*bucket: success=*/1, 0);
  }

  const std::string& http_method() { return api_call_flow_->http_method_; }

  const GURL& request_url() { return api_call_flow_->request_url_; }

  const std::string& serialized_request() {
    return api_call_flow_->serialized_request_;
  }

  // Returns |response_proto| as the result to the current API request.
  void FinishApiCallFlow(const google::protobuf::MessageLite* response_proto) {
    std::move(api_call_flow_->result_callback_)
        .Run(response_proto->SerializeAsString());
  }

  // Returns |serialized_proto| as the result to the current API request.
  void FinishApiCallFlowRaw(const std::string& serialized_proto) {
    std::move(api_call_flow_->result_callback_).Run(serialized_proto);
  }

  // Ends the current API request with |error|.
  void FailApiCallFlow(
      PushNotificationDesktopApiCallFlow::PushNotificationApiCallFlowError
          error) {
    std::move(api_call_flow_->error_callback_).Run(error);
  }

  push_notification::proto::NotificationsMultiLoginUpdateRequest
  CreateRequestProto() {
    push_notification::proto::NotificationsMultiLoginUpdateRequest
        request_proto;
    request_proto.mutable_target()->set_channel_type(
        push_notification::proto::ChannelType::GCM_DEVICE_PUSH);
    request_proto.mutable_target()
        ->mutable_delivery_address()
        ->mutable_gcm_device_address()
        ->set_registration_id(kRegistrationId);
    request_proto.mutable_target()
        ->mutable_delivery_address()
        ->mutable_gcm_device_address()
        ->set_application_id(kApplicationId);
    request_proto.add_registrations();

    request_proto.mutable_registrations(0)
        ->mutable_user_id()
        ->mutable_gaia_credentials()
        ->set_oauth_token(kToken);
    request_proto.set_client_id(kClientId);
    return request_proto;
  }

 protected:
  base::test::TaskEnvironment task_environment_;
  base::HistogramTester histogram_tester_;
  signin::IdentityTestEnvironment identity_test_environment_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_factory_;
  std::unique_ptr<PushNotificationServerClient> client_;
  raw_ptr<FakePushNotificationApiCallFlow> api_call_flow_;
};

TEST_F(PushNotificationServerClientDesktopImplTest,
       RegisterWithPushNotificationServiceSuccess) {
  base::test::TestFuture<
      const push_notification::proto::NotificationsMultiLoginUpdateResponse&>
      future;
  auto request_proto = CreateRequestProto();

  client_->RegisterWithPushNotificationService(
      request_proto, future.GetCallback(),
      base::BindOnce(&NotCalled<PushNotificationDesktopApiCallFlow::
                                    PushNotificationApiCallFlowError>));
  identity_test_environment_
      .WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
          kAccessToken, base::Time::Max());
  EXPECT_EQ(kPost, http_method());
  EXPECT_EQ(request_url(), GURL(std::string(kTestGoogleApisUrl) + "/v1/" +
                                std::string(kMultiLoginPath)));
  push_notification::proto::NotificationsMultiLoginUpdateRequest
      expected_request;
  EXPECT_TRUE(expected_request.ParseFromString(serialized_request()));
  EXPECT_EQ(kClientId, expected_request.client_id());
  push_notification::proto::NotificationsMultiLoginUpdateResponse
      response_proto;
  push_notification::proto::NotificationsMultiLoginUpdateResponse::
      RegistrationResult* registration_result =
          response_proto.add_registration_results();
  push_notification::proto::StatusProto* status =
      registration_result->mutable_status();
  status->set_code(0);
  status->set_message("OK");
  FinishApiCallFlow(&response_proto);
  // Check that the result received in callback is the same as the response.
  push_notification::proto::NotificationsMultiLoginUpdateResponse result_proto =
      future.Take();
  EXPECT_EQ(0, result_proto.registration_results(0).status().code());
  EXPECT_EQ("OK", result_proto.registration_results(0).status().message());
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: failure=*/0, 0);
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: success=*/1, 1);
}

TEST_F(PushNotificationServerClientDesktopImplTest,
       RegisterWithPushNotificationServiceFailure) {
  auto request_proto = CreateRequestProto();

  base::test::TestFuture<
      PushNotificationDesktopApiCallFlow::PushNotificationApiCallFlowError>
      future;
  client_->RegisterWithPushNotificationService(
      request_proto,
      base::BindOnce(
          &NotCalledConstRef<
              push_notification::proto::NotificationsMultiLoginUpdateResponse>),
      future.GetCallback());
  identity_test_environment_
      .WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
          kAccessToken, base::Time::Max());
  EXPECT_EQ(kPost, http_method());
  EXPECT_EQ(request_url(), GURL(std::string(kTestGoogleApisUrl) + "/v1/" +
                                std::string(kMultiLoginPath)));
  FailApiCallFlow(PushNotificationDesktopApiCallFlow::
                      PushNotificationApiCallFlowError::kInternalServerError);
  EXPECT_EQ(PushNotificationDesktopApiCallFlow::
                PushNotificationApiCallFlowError::kInternalServerError,
            future.Get());
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: failure=*/0, 0);
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: success=*/1, 1);
}

TEST_F(PushNotificationServerClientDesktopImplTest, FetchAccessTokenFailure) {
  base::test::TestFuture<
      PushNotificationDesktopApiCallFlow::PushNotificationApiCallFlowError>
      future;
  client_->RegisterWithPushNotificationService(
      push_notification::proto::NotificationsMultiLoginUpdateRequest(),
      base::BindOnce(
          &NotCalledConstRef<
              push_notification::proto::NotificationsMultiLoginUpdateResponse>),
      future.GetCallback());
  identity_test_environment_
      .WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
          GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE));
  EXPECT_EQ(PushNotificationDesktopApiCallFlow::
                PushNotificationApiCallFlowError::kAuthenticationError,
            future.Get());
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: failure=*/0, 1);
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: success=*/1, 0);
}

TEST_F(PushNotificationServerClientDesktopImplTest, ParseResponseProtoFailure) {
  auto request_proto = CreateRequestProto();

  base::test::TestFuture<
      PushNotificationDesktopApiCallFlow::PushNotificationApiCallFlowError>
      future;
  client_->RegisterWithPushNotificationService(
      request_proto,
      base::BindOnce(
          &NotCalledConstRef<
              push_notification::proto::NotificationsMultiLoginUpdateResponse>),
      future.GetCallback());
  identity_test_environment_
      .WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
          kAccessToken, base::Time::Max());
  EXPECT_EQ(kPost, http_method());
  EXPECT_EQ(request_url(), std::string(kTestGoogleApisUrl) + "/v1/" +
                               std::string(kMultiLoginPath));
  FinishApiCallFlowRaw("Not a valid serialized response message.");
  EXPECT_EQ(PushNotificationDesktopApiCallFlow::
                PushNotificationApiCallFlowError::kResponseMalformed,
            future.Get());
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: failure=*/0, 0);
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: success=*/1, 1);
}

TEST_F(PushNotificationServerClientDesktopImplTest,
       MakeSecondRequestBeforeFirstRequestSucceeds) {
  auto request_proto = CreateRequestProto();

  // Make first request.
  base::test::TestFuture<
      const push_notification::proto::NotificationsMultiLoginUpdateResponse&>
      future;
  client_->RegisterWithPushNotificationService(
      request_proto, future.GetCallback(),
      base::BindOnce(&NotCalled<PushNotificationDesktopApiCallFlow::
                                    PushNotificationApiCallFlowError>));
  identity_test_environment_
      .WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
          kAccessToken, base::Time::Max());
  EXPECT_EQ(kPost, http_method());
  EXPECT_EQ(request_url(), std::string(kTestGoogleApisUrl) + "/v1/" +
                               std::string(kMultiLoginPath));
  // With request pending, make second request.
  {
    base::test::TestFuture<
        PushNotificationDesktopApiCallFlow::PushNotificationApiCallFlowError>
        future2;
    EXPECT_DCHECK_DEATH(client_->RegisterWithPushNotificationService(
        push_notification::proto::NotificationsMultiLoginUpdateRequest(),
        base::BindOnce(
            &NotCalledConstRef<push_notification::proto::
                                   NotificationsMultiLoginUpdateResponse>),
        future2.GetCallback()));
  }
  // Complete first request.
  {
    push_notification::proto::NotificationsMultiLoginUpdateResponse
        response_proto;
    push_notification::proto::NotificationsMultiLoginUpdateResponse::
        RegistrationResult* registration_result =
            response_proto.add_registration_results();
    push_notification::proto::StatusProto* status =
        registration_result->mutable_status();
    status->set_code(0);
    status->set_message("OK");
    FinishApiCallFlow(&response_proto);
  }
  push_notification::proto::NotificationsMultiLoginUpdateResponse result_proto =
      future.Take();
  EXPECT_EQ(0, result_proto.registration_results(0).status().code());
  EXPECT_EQ("OK", result_proto.registration_results(0).status().message());
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: failure=*/0, 0);
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: success=*/1, 1);
}

TEST_F(PushNotificationServerClientDesktopImplTest,
       MakeSecondRequestAfterFirstRequestSucceeds) {
  // Make first request successfully.
  {
    base::test::TestFuture<
        const push_notification::proto::NotificationsMultiLoginUpdateResponse&>
        future;
    auto request_proto = CreateRequestProto();

    client_->RegisterWithPushNotificationService(
        request_proto, future.GetCallback(),
        base::BindOnce(&NotCalled<PushNotificationDesktopApiCallFlow::
                                      PushNotificationApiCallFlowError>));
    identity_test_environment_
        .WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
            kAccessToken, base::Time::Max());
    EXPECT_EQ(kPost, http_method());
    EXPECT_EQ(request_url(), std::string(kTestGoogleApisUrl) + "/v1/" +
                                 std::string(kMultiLoginPath));
    push_notification::proto::NotificationsMultiLoginUpdateResponse
        response_proto;
    push_notification::proto::NotificationsMultiLoginUpdateResponse::
        RegistrationResult* registration_result =
            response_proto.add_registration_results();
    push_notification::proto::StatusProto* status =
        registration_result->mutable_status();
    status->set_code(0);
    status->set_message("OK");
    FinishApiCallFlow(&response_proto);
    push_notification::proto::NotificationsMultiLoginUpdateResponse
        result_proto = future.Take();
    EXPECT_EQ(0, result_proto.registration_results(0).status().code());
    EXPECT_EQ("OK", result_proto.registration_results(0).status().message());
  }
  // Second request fails.
  {
    base::test::TestFuture<
        PushNotificationDesktopApiCallFlow::PushNotificationApiCallFlowError>
        future;
    EXPECT_DCHECK_DEATH(client_->RegisterWithPushNotificationService(
        push_notification::proto::NotificationsMultiLoginUpdateRequest(),
        base::BindOnce(
            &NotCalledConstRef<push_notification::proto::
                                   NotificationsMultiLoginUpdateResponse>),
        future.GetCallback()));
  }
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: failure=*/0, 0);
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: success=*/1, 1);
}

TEST_F(PushNotificationServerClientDesktopImplTest, GetAccessTokenUsed) {
  EXPECT_FALSE(client_->GetAccessTokenUsed().has_value());
  base::test::TestFuture<
      const push_notification::proto::NotificationsMultiLoginUpdateResponse&>
      future;
  auto request_proto = CreateRequestProto();

  client_->RegisterWithPushNotificationService(
      request_proto, future.GetCallback(),
      base::BindOnce(&NotCalled<PushNotificationDesktopApiCallFlow::
                                    PushNotificationApiCallFlowError>));
  identity_test_environment_
      .WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
          kAccessToken, base::Time::Max());
  EXPECT_EQ(kPost, http_method());
  EXPECT_EQ(request_url(), std::string(kTestGoogleApisUrl) + "/v1/" +
                               std::string(kMultiLoginPath));
  EXPECT_EQ(kAccessToken, client_->GetAccessTokenUsed().value());
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: failure=*/0, 0);
  histogram_tester_.ExpectBucketCount(kOAuthTokenRetrievalResult,
                                      /*bucket: success=*/1, 1);
}

}  // namespace push_notification