chromium/chrome/browser/nearby_sharing/tachyon_ice_config_fetcher_unittest.cc

// Copyright 2021 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/nearby_sharing/tachyon_ice_config_fetcher.h"

#include "base/logging.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 "chrome/browser/nearby_sharing/proto/duration.pb.h"
#include "chrome/browser/nearby_sharing/proto/ice.pb.h"
#include "chrome/browser/nearby_sharing/proto/tachyon.pb.h"
#include "chrome/browser/nearby_sharing/proto/tachyon_common.pb.h"
#include "chrome/browser/nearby_sharing/proto/tachyon_enums.pb.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

namespace tachyon_proto = nearbyshare::tachyon_proto;

const char kApiUrl[] =
    "https://instantmessaging-pa.googleapis.com/v1/peertopeer:geticeserver";
const char kOAuthToken[] = "oauth_token";
const char kTestAccount[] = "test@test.test";
const char kIceConfigFetchedMetric[] = "Sharing.WebRtc.IceConfigFetched";
const char kResultMetric[] =
    "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher.Result";
const char kFailureReasonMetric[] =
    "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher.FailureReason";
const char kCacheHitMetric[] =
    "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher.CacheHit";
const char kTokenFetchSuccessMetric[] =
    "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher."
    "OAuthTokenFetchResult";
const int kLifetimeDurationSeconds = 86400;

void CheckSuccessResponse(
    const std::vector<::sharing::mojom::IceServerPtr>& ice_servers) {
  ASSERT_EQ(2u, ice_servers.size());

  // First response doesnt have credentials.
  ASSERT_EQ(1u, ice_servers[0]->urls.size());
  ASSERT_FALSE(ice_servers[0]->username);
  ASSERT_FALSE(ice_servers[0]->credential);

  // Second response has credentials.
  ASSERT_EQ(2u, ice_servers[1]->urls.size());
  ASSERT_EQ("username", ice_servers[1]->username);
  ASSERT_EQ("credential", ice_servers[1]->credential);
}

}  // namespace

class TachyonIceConfigFetcherTest : public testing::Test {
 public:
  TachyonIceConfigFetcherTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
        test_shared_loader_factory_(
            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                &test_url_loader_factory_)),
        ice_config_fetcher_(identity_test_environment_.identity_manager(),
                            test_shared_loader_factory_) {
    identity_test_environment_.MakePrimaryAccountAvailable(
        kTestAccount, signin::ConsentLevel::kSignin);
  }
  ~TachyonIceConfigFetcherTest() override = default;

  std::string GetSuccessResponse() {
    tachyon_proto::GetICEServerResponse response;

    auto* config = response.mutable_ice_config();
    config->mutable_lifetime_duration()->set_seconds(kLifetimeDurationSeconds);
    auto* server1 = config->add_ice_servers();
    server1->add_urls("stun:url1");
    auto* server2 = config->add_ice_servers();
    server2->add_urls("turn:url2?transport=udp");
    server2->add_urls("turn:url3?transport=tcp");
    server2->set_username("username");
    server2->set_credential("credential");

    std::string output;
    response.SerializeToString(&output);
    return output;
  }

  void SetOAuthTokenSuccessful(bool success) {
    identity_test_environment_
        .WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
            success ? kOAuthToken : "", base::Time::Now());
  }

 protected:
  base::test::SingleThreadTaskEnvironment task_environment_;
  signin::IdentityTestEnvironment identity_test_environment_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
  TachyonIceConfigFetcher ice_config_fetcher_;
  base::HistogramTester histogram_tester_;
};

TEST_F(TachyonIceConfigFetcherTest, ResponseSuccessful) {
  base::RunLoop run_loop;
  ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(
      [&](std::vector<::sharing::mojom::IceServerPtr> ice_servers) {
        CheckSuccessResponse(ice_servers);
        run_loop.Quit();
      }));
  SetOAuthTokenSuccessful(true);

  std::string response = GetSuccessResponse();

  ASSERT_TRUE(test_url_loader_factory_.IsPending(kApiUrl, nullptr));

  test_url_loader_factory_.AddResponse(kApiUrl, response, net::HTTP_OK);
  run_loop.Run();

  histogram_tester_.ExpectTotalCount(kIceConfigFetchedMetric, 1);
  histogram_tester_.ExpectBucketCount(kIceConfigFetchedMetric, 2, 1);
  histogram_tester_.ExpectTotalCount(kResultMetric, 1);
  histogram_tester_.ExpectBucketCount(kResultMetric, 1, 1);
  histogram_tester_.ExpectTotalCount(kCacheHitMetric, 1);
  histogram_tester_.ExpectBucketCount(kCacheHitMetric, 0, 1);
  histogram_tester_.ExpectTotalCount(kFailureReasonMetric, 0);
  histogram_tester_.ExpectTotalCount(kTokenFetchSuccessMetric, 1);
  histogram_tester_.ExpectBucketCount(kTokenFetchSuccessMetric, 1, 1);
}

TEST_F(TachyonIceConfigFetcherTest, ResponseError) {
  base::RunLoop run_loop;
  ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(
      [&](std::vector<::sharing::mojom::IceServerPtr> ice_servers) {
        // Makes sure that we at least return default servers in case of an
        // error.
        EXPECT_FALSE(ice_servers.empty());
        run_loop.Quit();
      }));
  SetOAuthTokenSuccessful(true);

  ASSERT_TRUE(test_url_loader_factory_.IsPending(kApiUrl, nullptr));

  test_url_loader_factory_.AddResponse(kApiUrl, "",
                                       net::HTTP_INTERNAL_SERVER_ERROR);
  run_loop.Run();

  histogram_tester_.ExpectTotalCount(kIceConfigFetchedMetric, 1);
  histogram_tester_.ExpectBucketCount(kIceConfigFetchedMetric, 0, 1);
  histogram_tester_.ExpectTotalCount(kResultMetric, 1);
  histogram_tester_.ExpectBucketCount(kResultMetric, 0, 1);
  histogram_tester_.ExpectTotalCount(kCacheHitMetric, 1);
  histogram_tester_.ExpectBucketCount(kCacheHitMetric, 0, 1);
  histogram_tester_.ExpectTotalCount(kFailureReasonMetric, 1);
  histogram_tester_.ExpectBucketCount(kFailureReasonMetric, 500, 1);
  histogram_tester_.ExpectTotalCount(kTokenFetchSuccessMetric, 1);
  histogram_tester_.ExpectBucketCount(kTokenFetchSuccessMetric, 1, 1);
}

TEST_F(TachyonIceConfigFetcherTest, OverlappingCalls) {
  base::RunLoop run_loop;
  int counter = 2;
  auto callback = [&](std::vector<::sharing::mojom::IceServerPtr> ice_servers) {
    CheckSuccessResponse(ice_servers);
    counter -= 1;
    if (counter == 0) {
      run_loop.Quit();
    }
  };
  // First call.
  ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(callback));
  SetOAuthTokenSuccessful(true);

  // Second call overlaps before any responses are processed.
  ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(callback));
  SetOAuthTokenSuccessful(true);

  std::string response = GetSuccessResponse();

  ASSERT_TRUE(test_url_loader_factory_.IsPending(kApiUrl, nullptr));

  test_url_loader_factory_.AddResponse(kApiUrl, response, net::HTTP_OK);
  run_loop.Run();

  histogram_tester_.ExpectTotalCount(kIceConfigFetchedMetric, 2);
  histogram_tester_.ExpectBucketCount(kIceConfigFetchedMetric, 2, 2);
  histogram_tester_.ExpectTotalCount(kResultMetric, 2);
  histogram_tester_.ExpectBucketCount(kResultMetric, 1, 2);
  histogram_tester_.ExpectTotalCount(kCacheHitMetric, 2);
  histogram_tester_.ExpectBucketCount(kCacheHitMetric, 0, 2);
  histogram_tester_.ExpectTotalCount(kFailureReasonMetric, 0);
  histogram_tester_.ExpectTotalCount(kTokenFetchSuccessMetric, 2);
  histogram_tester_.ExpectBucketCount(kTokenFetchSuccessMetric, 1, 2);
}

TEST_F(TachyonIceConfigFetcherTest, IceServersCached) {
  auto callback = [](base::RunLoop* run_loop,
                     std::vector<::sharing::mojom::IceServerPtr> ice_servers) {
    CheckSuccessResponse(ice_servers);
    run_loop->Quit();
  };
  std::string response = GetSuccessResponse();

  // First call.
  auto run_loop = std::make_unique<base::RunLoop>();
  ice_config_fetcher_.GetIceServers(base::BindOnce(callback, run_loop.get()));
  SetOAuthTokenSuccessful(true);
  ASSERT_TRUE(test_url_loader_factory_.IsPending(kApiUrl));
  test_url_loader_factory_.SimulateResponseForPendingRequest(kApiUrl, response);

  // Complete first call before beginning second call
  run_loop->Run();

  // Second call returns cached result
  run_loop.reset(new base::RunLoop());
  ice_config_fetcher_.GetIceServers(base::BindOnce(callback, run_loop.get()));
  ASSERT_FALSE(test_url_loader_factory_.IsPending(kApiUrl));
  run_loop->Run();

  // Wait until the cache has expired.
  task_environment_.FastForwardBy(base::Seconds(kLifetimeDurationSeconds + 1));

  // Expired cache results in fetching the servers again.
  run_loop.reset(new base::RunLoop());
  ice_config_fetcher_.GetIceServers(base::BindOnce(callback, run_loop.get()));
  SetOAuthTokenSuccessful(true);
  ASSERT_TRUE(test_url_loader_factory_.IsPending(kApiUrl));
  test_url_loader_factory_.SimulateResponseForPendingRequest(kApiUrl, response);
  run_loop->Run();

  histogram_tester_.ExpectTotalCount(kIceConfigFetchedMetric, 2);
  histogram_tester_.ExpectBucketCount(kIceConfigFetchedMetric, 2, 2);
  histogram_tester_.ExpectTotalCount(kResultMetric, 2);
  histogram_tester_.ExpectBucketCount(kResultMetric, 1, 2);
  histogram_tester_.ExpectTotalCount(kCacheHitMetric, 3);
  histogram_tester_.ExpectBucketCount(kCacheHitMetric, 0, 2);
  histogram_tester_.ExpectTotalCount(kFailureReasonMetric, 0);
  histogram_tester_.ExpectTotalCount(kTokenFetchSuccessMetric, 2);
  histogram_tester_.ExpectBucketCount(kTokenFetchSuccessMetric, 1, 2);
}

TEST_F(TachyonIceConfigFetcherTest, OAuthTokenFailed) {
  base::RunLoop run_loop;
  ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(
      [&](std::vector<::sharing::mojom::IceServerPtr> ice_servers) {
        // Makes sure that we at least return default servers in case of an
        // error.
        EXPECT_FALSE(ice_servers.empty());
        run_loop.Quit();
      }));
  SetOAuthTokenSuccessful(false);
  ASSERT_EQ(0, test_url_loader_factory_.NumPending());
  run_loop.Run();

  histogram_tester_.ExpectTotalCount(kIceConfigFetchedMetric, 0);
  histogram_tester_.ExpectTotalCount(kResultMetric, 0);
  histogram_tester_.ExpectTotalCount(kCacheHitMetric, 1);
  histogram_tester_.ExpectBucketCount(kCacheHitMetric, 0, 1);
  histogram_tester_.ExpectTotalCount(kFailureReasonMetric, 0);
  histogram_tester_.ExpectTotalCount(kTokenFetchSuccessMetric, 1);
  histogram_tester_.ExpectBucketCount(kTokenFetchSuccessMetric, 0, 1);
}

TEST_F(TachyonIceConfigFetcherTest, OverlappingTokenFetch) {
  base::RunLoop run_loop;
  int counter = 2;
  auto callback = [&](std::vector<::sharing::mojom::IceServerPtr> ice_servers) {
    CheckSuccessResponse(ice_servers);
    counter -= 1;
    if (counter == 0) {
      run_loop.Quit();
    }
  };
  // First call.
  ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(callback));

  // Second call overlaps before the first has an OAuth token.
  ice_config_fetcher_.GetIceServers(base::BindLambdaForTesting(callback));

  // Return an OAuth token for both requests.
  SetOAuthTokenSuccessful(true);

  std::string response = GetSuccessResponse();

  ASSERT_TRUE(test_url_loader_factory_.IsPending(kApiUrl, nullptr));

  test_url_loader_factory_.AddResponse(kApiUrl, response, net::HTTP_OK);
  run_loop.Run();

  histogram_tester_.ExpectTotalCount(kIceConfigFetchedMetric, 2);
  histogram_tester_.ExpectBucketCount(kIceConfigFetchedMetric, 2, 2);
  histogram_tester_.ExpectTotalCount(kResultMetric, 2);
  histogram_tester_.ExpectBucketCount(kResultMetric, 1, 2);
  histogram_tester_.ExpectTotalCount(kCacheHitMetric, 2);
  histogram_tester_.ExpectBucketCount(kCacheHitMetric, 0, 2);
  histogram_tester_.ExpectTotalCount(kFailureReasonMetric, 0);
  histogram_tester_.ExpectTotalCount(kTokenFetchSuccessMetric, 2);
  histogram_tester_.ExpectBucketCount(kTokenFetchSuccessMetric, 1, 2);
}