chromium/chrome/browser/ash/net/network_diagnostics/video_conferencing_routine_unittest.cc

// Copyright 2020 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/ash/net/network_diagnostics/video_conferencing_routine.h"

#include <deque>
#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/run_loop.h"
#include "base/time/time.h"
#include "chrome/browser/ash/net/network_diagnostics/network_diagnostics_util.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {
namespace network_diagnostics {

namespace {

namespace mojom = ::chromeos::network_diagnostics::mojom;

// Test implementation of UdpProber.
class TestUdpProber final : public UdpProber {
 public:
  TestUdpProber(UdpProber::UdpProbeCompleteCallback callback,
                int result,
                UdpProber::ProbeExitEnum probe_exit_enum) {
    // Post an asynchronous task simulating a completed probe. This mimics the
    // behavior of the production UdpProber constructor since the TestUdpProber
    // instance will be complete before FinishProbe is invoked. In the
    // production UdpProber, the constructor completes before DNS host
    // resolution is complete.
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&TestUdpProber::FinishProbe, weak_factory_.GetWeakPtr(),
                       std::move(callback), result, probe_exit_enum));
  }

  TestUdpProber(const TestUdpProber&) = delete;
  TestUdpProber& operator=(const TestUdpProber&) = delete;
  ~TestUdpProber() override = default;

 private:
  void FinishProbe(UdpProber::UdpProbeCompleteCallback callback,
                   int result,
                   UdpProber::ProbeExitEnum probe_exit_enum) {
    std::move(callback).Run(result, probe_exit_enum);
  }

  base::WeakPtrFactory<TestUdpProber> weak_factory_{this};
};

// Test implementation of TlsProber.
class TestTlsProber final : public TlsProber {
 public:
  TestTlsProber(TlsProber::TlsProbeCompleteCallback callback,
                int result,
                TlsProber::ProbeExitEnum probe_exit_enum) {
    // Post an asynchronous task simulating a completed probe. This mimics the
    // behavior of the production TlsProber constructor since the TestTlsProber
    // instance will be complete before FinishProbe is invoked. In the
    // production TlsProber, the constructor completes before DNS host
    // resolution is complete.
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&TestTlsProber::FinishProbe, weak_factory_.GetWeakPtr(),
                       std::move(callback), result, probe_exit_enum));
  }

  TestTlsProber(const TestTlsProber&) = delete;
  TestTlsProber& operator=(const TestTlsProber&) = delete;
  ~TestTlsProber() override = default;

 private:
  void FinishProbe(TlsProber::TlsProbeCompleteCallback callback,
                   int result,
                   TlsProber::ProbeExitEnum probe_exit_enum) {
    std::move(callback).Run(result, probe_exit_enum);
  }

  base::WeakPtrFactory<TestTlsProber> weak_factory_{this};
};

}  // namespace

class VideoConferencingRoutineTest : public ::testing::Test {
 public:
  struct UdpProberReturnValue {
    net::Error result;
    UdpProber::ProbeExitEnum probe_exit_enum;
  };
  struct TlsProberReturnValue {
    net::Error result;
    TlsProber::ProbeExitEnum probe_exit_enum;
  };

  VideoConferencingRoutineTest() = default;
  VideoConferencingRoutineTest(const VideoConferencingRoutineTest&) = delete;
  VideoConferencingRoutineTest& operator=(const VideoConferencingRoutineTest&) =
      delete;
  ~VideoConferencingRoutineTest() override = default;

  void CompareResult(
      mojom::RoutineVerdict expected_verdict,
      const std::vector<mojom::VideoConferencingProblem>& expected_problems,
      mojom::RoutineResultPtr result) {
    EXPECT_EQ(expected_verdict, result->verdict);
    EXPECT_EQ(expected_problems,
              result->problems->get_video_conferencing_problems());
    run_loop_.Quit();
  }

  std::unique_ptr<UdpProber> CreateAndExecuteUdpProber(
      network::NetworkContextGetter network_context_getter,
      net::HostPortPair host_port_pair,
      base::span<const uint8_t> data,
      net::NetworkTrafficAnnotationTag tag,
      base::TimeDelta timeout_after_host_resolution,
      UdpProber::UdpProbeCompleteCallback callback) {
    DCHECK(fake_udp_probe_results_.size() > 0);

    auto value = fake_udp_probe_results_.front();
    fake_udp_probe_results_.pop_front();
    auto test_udp_prober = std::make_unique<TestUdpProber>(
        std::move(callback), value.result, value.probe_exit_enum);
    return std::move(test_udp_prober);
  }

  std::unique_ptr<TlsProber> CreateAndExecuteTlsProber(
      network::NetworkContextGetter network_context_getter,
      net::HostPortPair host_port_pair,
      bool negotiate_tls,
      TlsProber::TlsProbeCompleteCallback callback) {
    DCHECK(fake_tls_probe_results_.size() > 0);

    auto value = fake_tls_probe_results_.front();
    fake_tls_probe_results_.pop_front();
    auto test_tls_prober = std::make_unique<TestTlsProber>(
        std::move(callback), value.result, value.probe_exit_enum);
    return std::move(test_tls_prober);
  }

 protected:
  void RunRoutine(
      mojom::RoutineVerdict expected_routine_verdict,
      const std::vector<mojom::VideoConferencingProblem>& expected_problems) {
    video_conferencing_routine_->RunRoutine(
        base::BindOnce(&VideoConferencingRoutineTest::CompareResult, weak_ptr(),
                       expected_routine_verdict, expected_problems));
    run_loop_.Run();
  }

  void SetUpRoutine(std::deque<UdpProberReturnValue> fake_udp_probe_results,
                    std::deque<TlsProberReturnValue> fake_tls_probe_results) {
    fake_udp_probe_results_ = std::move(fake_udp_probe_results);
    fake_tls_probe_results_ = std::move(fake_tls_probe_results);
    video_conferencing_routine_ = std::make_unique<VideoConferencingRoutine>(
        mojom::RoutineCallSource::kDiagnosticsUI);
    video_conferencing_routine_->set_udp_prober_getter_callback_for_testing(
        base::BindRepeating(
            &VideoConferencingRoutineTest::CreateAndExecuteUdpProber,
            base::Unretained(this)));
    video_conferencing_routine_->set_tls_prober_getter_callback_for_testing(
        base::BindRepeating(
            &VideoConferencingRoutineTest::CreateAndExecuteTlsProber,
            base::Unretained(this)));
  }

  // Sets up required properties (via fakes) and runs the test.
  //
  // Parameters:
  // |fake_udp_probe_results|: Represents the results of UDP probes.
  // |fake_tls_probe_results|: Represents the results of TCP and TLS probes.
  // |expected_routine_verdict|: Represents the expected verdict
  // reported by this test.
  void SetUpAndRunRoutine(
      std::deque<UdpProberReturnValue> fake_udp_probe_results,
      std::deque<TlsProberReturnValue> fake_tls_probe_results,
      mojom::RoutineVerdict expected_routine_verdict,
      const std::vector<mojom::VideoConferencingProblem>& expected_problems) {
    SetUpRoutine(std::move(fake_udp_probe_results),
                 std::move(fake_tls_probe_results));
    RunRoutine(expected_routine_verdict, expected_problems);
  }

  base::WeakPtr<VideoConferencingRoutineTest> weak_ptr() {
    return weak_factory_.GetWeakPtr();
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
  base::RunLoop run_loop_;
  std::deque<UdpProberReturnValue> fake_udp_probe_results_;
  std::deque<TlsProberReturnValue> fake_tls_probe_results_;
  std::unique_ptr<VideoConferencingRoutine> video_conferencing_routine_;
  base::WeakPtrFactory<VideoConferencingRoutineTest> weak_factory_{this};
};

// Tests the scenario where:
// (1) An open port to the STUN server is found via UDP and TCP.
// (2) All media hostnames are reachable.
TEST_F(VideoConferencingRoutineTest, TestSuccessfulPath) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    if (i < num_udp_ports_tested - 1) {
      fake_udp_probe_results.push_back(UdpProberReturnValue{
          net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
      continue;
    }
    fake_udp_probe_results.push_back(
        UdpProberReturnValue{net::OK, UdpProber::ProbeExitEnum::kSuccess});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    if (i < num_tcp_ports_tested - 1) {
      fake_tls_probe_results.push_back(TlsProberReturnValue{
          net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
      continue;
    }
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  // Results corresponding to the TLS requests to media hostnames.
  int num_media_hostnames_tested = util::GetDefaultMediaUrls().size();
  for (int i = 0; i < num_media_hostnames_tested; i++) {
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kNoProblem,
                     /*expected_problems=*/{});
}

// Tests the scenario where:
// (1) No open port to the STUN server is found via UDP.
TEST_F(VideoConferencingRoutineTest, TestUdpFailure) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    fake_udp_probe_results.push_back(UdpProberReturnValue{
        net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    if (i < num_tcp_ports_tested - 1) {
      fake_tls_probe_results.push_back(TlsProberReturnValue{
          net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
      continue;
    }
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  // Results corresponding to the TLS requests to media hostnames.
  int num_media_hostnames_tested = util::GetDefaultMediaUrls().size();
  for (int i = 0; i < num_media_hostnames_tested; i++) {
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kProblem,
                     {mojom::VideoConferencingProblem::kUdpFailure});
}

// Tests the scenario where:
// (1) No open port to the STUN server is found via TCP.
TEST_F(VideoConferencingRoutineTest, TestTcpFailure) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    if (i < num_udp_ports_tested - 1) {
      fake_udp_probe_results.push_back(UdpProberReturnValue{
          net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
      continue;
    }
    fake_udp_probe_results.push_back(
        UdpProberReturnValue{net::OK, UdpProber::ProbeExitEnum::kSuccess});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    fake_tls_probe_results.push_back(TlsProberReturnValue{
        net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
  }

  // Results corresponding to the TLS requests to media hostnames.
  int num_media_hostnames_tested = util::GetDefaultMediaUrls().size();
  for (int i = 0; i < num_media_hostnames_tested; i++) {
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kProblem,
                     {mojom::VideoConferencingProblem::kTcpFailure});
}

// Tests the scenario where:
// (1) Requests to one or more media hostnames failed.
TEST_F(VideoConferencingRoutineTest, TestMediaFailure) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    if (i < num_udp_ports_tested - 1) {
      fake_udp_probe_results.push_back(UdpProberReturnValue{
          net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
      continue;
    }
    fake_udp_probe_results.push_back(
        UdpProberReturnValue{net::OK, UdpProber::ProbeExitEnum::kSuccess});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    if (i < num_tcp_ports_tested - 1) {
      fake_tls_probe_results.push_back(TlsProberReturnValue{
          net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
      continue;
    }
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  // Results corresponding to the TLS requests to media hostnames.
  fake_tls_probe_results.push_back(TlsProberReturnValue{
      net::ERR_FAILED, TlsProber::ProbeExitEnum::kTlsUpgradeFailure});

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kProblem,
                     {mojom::VideoConferencingProblem::kMediaFailure});
}

// Tests the scenario where:
// (1) No open port to the STUN server is found via UDP.
// (2) No open port to the STUN server is found via TCP.
TEST_F(VideoConferencingRoutineTest, TestUdpAndTcpFailure) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    fake_udp_probe_results.push_back(UdpProberReturnValue{
        net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    fake_tls_probe_results.push_back(TlsProberReturnValue{
        net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
  }

  // Results corresponding to the TLS requests to media hostnames.
  int num_media_hostnames_tested = util::GetDefaultMediaUrls().size();
  for (int i = 0; i < num_media_hostnames_tested; i++) {
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kProblem,
                     {mojom::VideoConferencingProblem::kUdpFailure,
                      mojom::VideoConferencingProblem::kTcpFailure});
}

// Tests the scenario where:
// (1) No open port to the STUN server is found via UDP.
// (2) Requests to one or more media hostnames failed.
TEST_F(VideoConferencingRoutineTest, TestUdpAndMediaFailure) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    fake_udp_probe_results.push_back(UdpProberReturnValue{
        net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    if (i < num_tcp_ports_tested - 1) {
      fake_tls_probe_results.push_back(TlsProberReturnValue{
          net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
      continue;
    }
    fake_tls_probe_results.push_back(
        TlsProberReturnValue{net::OK, TlsProber::ProbeExitEnum::kSuccess});
  }

  // Results corresponding to the TLS requests to media hostnames.
  fake_tls_probe_results.push_back(TlsProberReturnValue{
      net::ERR_FAILED, TlsProber::ProbeExitEnum::kTlsUpgradeFailure});

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kProblem,
                     {mojom::VideoConferencingProblem::kUdpFailure,
                      mojom::VideoConferencingProblem::kMediaFailure});
}

// Tests the scenario where:
// (1) No open port to the STUN server is found via TCP.
// (2) Requests to one or more media hostnames failed.
TEST_F(VideoConferencingRoutineTest, TestTcpAndMediaFailure) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    if (i < num_udp_ports_tested - 1) {
      fake_udp_probe_results.push_back(UdpProberReturnValue{
          net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
      continue;
    }
    fake_udp_probe_results.push_back(
        UdpProberReturnValue{net::OK, UdpProber::ProbeExitEnum::kSuccess});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    fake_tls_probe_results.push_back(TlsProberReturnValue{
        net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
  }

  // Results corresponding to the TLS requests to media hostnames.
  fake_tls_probe_results.push_back(TlsProberReturnValue{
      net::ERR_FAILED, TlsProber::ProbeExitEnum::kTlsUpgradeFailure});

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kProblem,
                     {mojom::VideoConferencingProblem::kTcpFailure,
                      mojom::VideoConferencingProblem::kMediaFailure});
}

// Tests the scenario where:
// (1) No open port to the STUN server is found via UDP.
// (2) No open port to the STUN server is found via TCP.
// (3) Requests to one or more media hostnames failed.
TEST_F(VideoConferencingRoutineTest, TestTcpAndUdpAndMediaFailure) {
  // Results corresponding to UDP requests to the STUN server.
  int num_udp_ports_tested = util::GetUdpPortsForGoogleStunServer().size();
  std::deque<UdpProberReturnValue> fake_udp_probe_results;
  for (int i = 0; i < num_udp_ports_tested; i++) {
    fake_udp_probe_results.push_back(UdpProberReturnValue{
        net::ERR_FAILED, UdpProber::ProbeExitEnum::kNoDataReceivedFailure});
  }

  // Tracks the results corresponding to TCP requests to the STUN server and
  // TLS requests to media hostnames.
  std::deque<TlsProberReturnValue> fake_tls_probe_results;

  // Results corresponding to the STUN server via TCP.
  int num_tcp_ports_tested = util::GetTcpPortsForGoogleStunServer().size();
  for (int i = 0; i < num_tcp_ports_tested; i++) {
    fake_tls_probe_results.push_back(TlsProberReturnValue{
        net::ERR_FAILED, TlsProber::ProbeExitEnum::kTcpConnectionFailure});
  }

  // Results corresponding to the TLS requests to media hostnames.
  fake_tls_probe_results.push_back(TlsProberReturnValue{
      net::ERR_FAILED, TlsProber::ProbeExitEnum::kTlsUpgradeFailure});

  SetUpAndRunRoutine(std::move(fake_udp_probe_results),
                     std::move(fake_tls_probe_results),
                     mojom::RoutineVerdict::kProblem,
                     {mojom::VideoConferencingProblem::kUdpFailure,
                      mojom::VideoConferencingProblem::kTcpFailure,
                      mojom::VideoConferencingProblem::kMediaFailure});
}

}  // namespace network_diagnostics
}  // namespace ash