Reland "Rewrite WebRtcSession BUNDLE tests as PeerConnection tests"

This is a reland of b49b66109ea8a0a33a3192ebccf91366af2e49ae.

Original change's description:
> Rewrite WebRtcSession BUNDLE tests as PeerConnection tests
> 
> Bug: webrtc:8222
> Change-Id: Id47e4544dc073564ad7e63d02865ca80dd5a85ff
> Reviewed-on: https://webrtc-review.googlesource.com/8280
> Commit-Queue: Steve Anton <steveanton@webrtc.org>
> Reviewed-by: Taylor Brandstetter <deadbeef@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#20365}

Bug: webrtc:8222
Change-Id: If3dcd8090875c641881e2b9e92fc1db387ba1de5
Reviewed-on: https://webrtc-review.googlesource.com/14400
Commit-Queue: Steve Anton <steveanton@webrtc.org>
Reviewed-by: Taylor Brandstetter <deadbeef@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#20397}
diff --git a/pc/peerconnection_bundle_unittest.cc b/pc/peerconnection_bundle_unittest.cc
new file mode 100644
index 0000000..1ade191
--- /dev/null
+++ b/pc/peerconnection_bundle_unittest.cc
@@ -0,0 +1,616 @@
+/*
+ *  Copyright 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "api/peerconnectionproxy.h"
+#include "p2p/base/fakeportallocator.h"
+#include "p2p/base/teststunserver.h"
+#include "p2p/client/basicportallocator.h"
+#include "pc/mediasession.h"
+#include "pc/peerconnection.h"
+#include "pc/peerconnectionwrapper.h"
+#include "pc/sdputils.h"
+#ifdef WEBRTC_ANDROID
+#include "pc/test/androidtestinitializer.h"
+#endif
+#include "pc/test/fakeaudiocapturemodule.h"
+#include "rtc_base/fakenetwork.h"
+#include "rtc_base/gunit.h"
+#include "rtc_base/ptr_util.h"
+#include "rtc_base/virtualsocketserver.h"
+#include "test/gmock.h"
+
+namespace webrtc {
+
+using BundlePolicy = PeerConnectionInterface::BundlePolicy;
+using RTCConfiguration = PeerConnectionInterface::RTCConfiguration;
+using RTCOfferAnswerOptions = PeerConnectionInterface::RTCOfferAnswerOptions;
+using RtcpMuxPolicy = PeerConnectionInterface::RtcpMuxPolicy;
+using rtc::SocketAddress;
+using ::testing::ElementsAre;
+using ::testing::UnorderedElementsAre;
+using ::testing::Values;
+
+constexpr int kDefaultTimeout = 10000;
+
+// TODO(steveanton): These tests should be rewritten to use the standard
+// RtpSenderInterface/DtlsTransportInterface objects once they're available in
+// the API. The RtpSender can be used to determine which transport a given media
+// will use: https://www.w3.org/TR/webrtc/#dom-rtcrtpsender-transport
+
+class PeerConnectionWrapperForBundleTest : public PeerConnectionWrapper {
+ public:
+  using PeerConnectionWrapper::PeerConnectionWrapper;
+
+  bool AddIceCandidateToMedia(cricket::Candidate* candidate,
+                              cricket::MediaType media_type) {
+    auto* desc = pc()->remote_description()->description();
+    for (size_t i = 0; i < desc->contents().size(); i++) {
+      const auto& content = desc->contents()[i];
+      auto* media_desc =
+          static_cast<cricket::MediaContentDescription*>(content.description);
+      if (media_desc->type() == media_type) {
+        candidate->set_transport_name(content.name);
+        JsepIceCandidate jsep_candidate(content.name, i, *candidate);
+        return pc()->AddIceCandidate(&jsep_candidate);
+      }
+    }
+    RTC_NOTREACHED();
+    return false;
+  }
+
+  rtc::PacketTransportInternal* voice_rtp_transport_channel() {
+    return (voice_channel() ? voice_channel()->rtp_dtls_transport() : nullptr);
+  }
+
+  rtc::PacketTransportInternal* voice_rtcp_transport_channel() {
+    return (voice_channel() ? voice_channel()->rtcp_dtls_transport() : nullptr);
+  }
+
+  cricket::VoiceChannel* voice_channel() {
+    return GetInternalPeerConnection()->voice_channel();
+  }
+
+  rtc::PacketTransportInternal* video_rtp_transport_channel() {
+    return (video_channel() ? video_channel()->rtp_dtls_transport() : nullptr);
+  }
+
+  rtc::PacketTransportInternal* video_rtcp_transport_channel() {
+    return (video_channel() ? video_channel()->rtcp_dtls_transport() : nullptr);
+  }
+
+  cricket::VideoChannel* video_channel() {
+    return GetInternalPeerConnection()->video_channel();
+  }
+
+  PeerConnection* GetInternalPeerConnection() {
+    auto* pci = reinterpret_cast<
+        PeerConnectionProxyWithInternal<PeerConnectionInterface>*>(pc());
+    return reinterpret_cast<PeerConnection*>(pci->internal());
+  }
+
+  // Returns true if the stats indicate that an ICE connection is either in
+  // progress or established with the given remote address.
+  bool HasConnectionWithRemoteAddress(const SocketAddress& address) {
+    auto report = GetStats();
+    if (!report) {
+      return false;
+    }
+    std::string matching_candidate_id;
+    for (auto* ice_candidate_stats :
+         report->GetStatsOfType<RTCRemoteIceCandidateStats>()) {
+      if (*ice_candidate_stats->ip == address.HostAsURIString() &&
+          *ice_candidate_stats->port == address.port()) {
+        matching_candidate_id = ice_candidate_stats->id();
+        break;
+      }
+    }
+    if (matching_candidate_id.empty()) {
+      return false;
+    }
+    for (auto* pair_stats :
+         report->GetStatsOfType<RTCIceCandidatePairStats>()) {
+      if (*pair_stats->remote_candidate_id == matching_candidate_id) {
+        if (*pair_stats->state == RTCStatsIceCandidatePairState::kInProgress ||
+            *pair_stats->state == RTCStatsIceCandidatePairState::kSucceeded) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  rtc::FakeNetworkManager* network() { return network_; }
+
+  void set_network(rtc::FakeNetworkManager* network) { network_ = network; }
+
+ private:
+  rtc::FakeNetworkManager* network_;
+};
+
+class PeerConnectionBundleTest : public ::testing::Test {
+ protected:
+  typedef std::unique_ptr<PeerConnectionWrapperForBundleTest> WrapperPtr;
+
+  PeerConnectionBundleTest()
+      : vss_(new rtc::VirtualSocketServer()), main_(vss_.get()) {
+#ifdef WEBRTC_ANDROID
+    InitializeAndroidObjects();
+#endif
+    pc_factory_ = CreatePeerConnectionFactory(
+        rtc::Thread::Current(), rtc::Thread::Current(), rtc::Thread::Current(),
+        FakeAudioCaptureModule::Create(), nullptr, nullptr);
+  }
+
+  WrapperPtr CreatePeerConnection() {
+    return CreatePeerConnection(RTCConfiguration());
+  }
+
+  WrapperPtr CreatePeerConnection(const RTCConfiguration& config) {
+    auto* fake_network = NewFakeNetwork();
+    auto port_allocator =
+        rtc::MakeUnique<cricket::BasicPortAllocator>(fake_network);
+    port_allocator->set_flags(cricket::PORTALLOCATOR_DISABLE_TCP |
+                              cricket::PORTALLOCATOR_DISABLE_RELAY);
+    port_allocator->set_step_delay(cricket::kMinimumStepDelay);
+    auto observer = rtc::MakeUnique<MockPeerConnectionObserver>();
+    auto pc = pc_factory_->CreatePeerConnection(
+        config, std::move(port_allocator), nullptr, observer.get());
+    if (!pc) {
+      return nullptr;
+    }
+
+    auto wrapper = rtc::MakeUnique<PeerConnectionWrapperForBundleTest>(
+        pc_factory_, pc, std::move(observer));
+    wrapper->set_network(fake_network);
+    return wrapper;
+  }
+
+  // Accepts the same arguments as CreatePeerConnection and adds default audio
+  // and video tracks.
+  template <typename... Args>
+  WrapperPtr CreatePeerConnectionWithAudioVideo(Args&&... args) {
+    auto wrapper = CreatePeerConnection(std::forward<Args>(args)...);
+    if (!wrapper) {
+      return nullptr;
+    }
+    wrapper->AddAudioTrack("a");
+    wrapper->AddVideoTrack("v");
+    return wrapper;
+  }
+
+  cricket::Candidate CreateLocalUdpCandidate(
+      const rtc::SocketAddress& address) {
+    cricket::Candidate candidate;
+    candidate.set_component(cricket::ICE_CANDIDATE_COMPONENT_DEFAULT);
+    candidate.set_protocol(cricket::UDP_PROTOCOL_NAME);
+    candidate.set_address(address);
+    candidate.set_type(cricket::LOCAL_PORT_TYPE);
+    return candidate;
+  }
+
+  rtc::FakeNetworkManager* NewFakeNetwork() {
+    // The PeerConnection's port allocator is tied to the PeerConnection's
+    // lifetime and expects the underlying NetworkManager to outlive it. If
+    // PeerConnectionWrapper owned the NetworkManager, it would be destroyed
+    // before the PeerConnection (since subclass members are destroyed before
+    // base class members). Therefore, the test fixture will own all the fake
+    // networks even though tests should access the fake network through the
+    // PeerConnectionWrapper.
+    auto* fake_network = new rtc::FakeNetworkManager();
+    fake_networks_.emplace_back(fake_network);
+    return fake_network;
+  }
+
+  std::unique_ptr<rtc::VirtualSocketServer> vss_;
+  rtc::AutoSocketServerThread main_;
+  rtc::scoped_refptr<PeerConnectionFactoryInterface> pc_factory_;
+  std::vector<std::unique_ptr<rtc::FakeNetworkManager>> fake_networks_;
+};
+
+SdpContentMutator RemoveRtcpMux() {
+  return [](cricket::ContentInfo* content, cricket::TransportInfo* transport) {
+    auto* media_desc =
+        static_cast<cricket::MediaContentDescription*>(content->description);
+    media_desc->set_rtcp_mux(false);
+  };
+}
+
+std::vector<int> GetCandidateComponents(
+    const std::vector<IceCandidateInterface*> candidates) {
+  std::vector<int> components;
+  for (auto* candidate : candidates) {
+    components.push_back(candidate->candidate().component());
+  }
+  return components;
+}
+
+// Test that there are 2 local UDP candidates (1 RTP and 1 RTCP candidate) for
+// each media section when disabling bundling and disabling RTCP multiplexing.
+TEST_F(PeerConnectionBundleTest,
+       TwoCandidatesForEachTransportWhenNoBundleNoRtcpMux) {
+  const SocketAddress kCallerAddress("1.1.1.1", 0);
+  const SocketAddress kCalleeAddress("2.2.2.2", 0);
+
+  RTCConfiguration config;
+  config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyNegotiate;
+  auto caller = CreatePeerConnectionWithAudioVideo(config);
+  caller->network()->AddInterface(kCallerAddress);
+  auto callee = CreatePeerConnectionWithAudioVideo(config);
+  callee->network()->AddInterface(kCalleeAddress);
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  RTCOfferAnswerOptions options_no_bundle;
+  options_no_bundle.use_rtp_mux = false;
+  auto answer = callee->CreateAnswer(options_no_bundle);
+  SdpContentsForEach(RemoveRtcpMux(), answer->description());
+  ASSERT_TRUE(
+      callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+  ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+
+  // Check that caller has separate RTP and RTCP candidates for each media.
+  EXPECT_TRUE_WAIT(caller->IsIceGatheringDone(), kDefaultTimeout);
+  EXPECT_THAT(
+      GetCandidateComponents(caller->observer()->GetCandidatesByMline(0)),
+      UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
+                           cricket::ICE_CANDIDATE_COMPONENT_RTCP));
+  EXPECT_THAT(
+      GetCandidateComponents(caller->observer()->GetCandidatesByMline(1)),
+      UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
+                           cricket::ICE_CANDIDATE_COMPONENT_RTCP));
+
+  // Check that callee has separate RTP and RTCP candidates for each media.
+  EXPECT_TRUE_WAIT(callee->IsIceGatheringDone(), kDefaultTimeout);
+  EXPECT_THAT(
+      GetCandidateComponents(callee->observer()->GetCandidatesByMline(0)),
+      UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
+                           cricket::ICE_CANDIDATE_COMPONENT_RTCP));
+  EXPECT_THAT(
+      GetCandidateComponents(callee->observer()->GetCandidatesByMline(1)),
+      UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
+                           cricket::ICE_CANDIDATE_COMPONENT_RTCP));
+}
+
+// Test that there is 1 local UDP candidate for both RTP and RTCP for each media
+// section when disabling bundle but enabling RTCP multiplexing.
+TEST_F(PeerConnectionBundleTest,
+       OneCandidateForEachTransportWhenNoBundleButRtcpMux) {
+  const SocketAddress kCallerAddress("1.1.1.1", 0);
+
+  auto caller = CreatePeerConnectionWithAudioVideo();
+  caller->network()->AddInterface(kCallerAddress);
+  auto callee = CreatePeerConnectionWithAudioVideo();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  RTCOfferAnswerOptions options_no_bundle;
+  options_no_bundle.use_rtp_mux = false;
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswer(options_no_bundle)));
+
+  EXPECT_TRUE_WAIT(caller->IsIceGatheringDone(), kDefaultTimeout);
+
+  EXPECT_EQ(1u, caller->observer()->GetCandidatesByMline(0).size());
+  EXPECT_EQ(1u, caller->observer()->GetCandidatesByMline(1).size());
+}
+
+// Test that there is 1 local UDP candidate in only the first media section when
+// bundling and enabling RTCP multiplexing.
+TEST_F(PeerConnectionBundleTest,
+       OneCandidateOnlyOnFirstTransportWhenBundleAndRtcpMux) {
+  const SocketAddress kCallerAddress("1.1.1.1", 0);
+
+  RTCConfiguration config;
+  config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
+  auto caller = CreatePeerConnectionWithAudioVideo(config);
+  caller->network()->AddInterface(kCallerAddress);
+  auto callee = CreatePeerConnectionWithAudioVideo(config);
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(caller->SetRemoteDescription(callee->CreateAnswer()));
+
+  EXPECT_TRUE_WAIT(caller->IsIceGatheringDone(), kDefaultTimeout);
+
+  EXPECT_EQ(1u, caller->observer()->GetCandidatesByMline(0).size());
+  EXPECT_EQ(0u, caller->observer()->GetCandidatesByMline(1).size());
+}
+
+// The following parameterized test verifies that an offer/answer with varying
+// bundle policies and either bundle in the answer or not will produce the
+// expected RTP transports for audio and video. In particular, for bundling we
+// care about whether they are separate transports or the same.
+
+enum class BundleIncluded { kBundleInAnswer, kBundleNotInAnswer };
+std::ostream& operator<<(std::ostream& out, BundleIncluded value) {
+  switch (value) {
+    case BundleIncluded::kBundleInAnswer:
+      return out << "bundle in answer";
+    case BundleIncluded::kBundleNotInAnswer:
+      return out << "bundle not in answer";
+  }
+  return out << "unknown";
+}
+
+class PeerConnectionBundleMatrixTest
+    : public PeerConnectionBundleTest,
+      public ::testing::WithParamInterface<
+          std::tuple<BundlePolicy, BundleIncluded, bool, bool>> {
+ protected:
+  PeerConnectionBundleMatrixTest() {
+    bundle_policy_ = std::get<0>(GetParam());
+    bundle_included_ = std::get<1>(GetParam());
+    expected_same_before_ = std::get<2>(GetParam());
+    expected_same_after_ = std::get<3>(GetParam());
+  }
+
+  PeerConnectionInterface::BundlePolicy bundle_policy_;
+  BundleIncluded bundle_included_;
+  bool expected_same_before_;
+  bool expected_same_after_;
+};
+
+TEST_P(PeerConnectionBundleMatrixTest,
+       VerifyTransportsBeforeAndAfterSettingRemoteAnswer) {
+  RTCConfiguration config;
+  config.bundle_policy = bundle_policy_;
+  auto caller = CreatePeerConnectionWithAudioVideo(config);
+  auto callee = CreatePeerConnectionWithAudioVideo();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  bool equal_before = (caller->voice_rtp_transport_channel() ==
+                       caller->video_rtp_transport_channel());
+  EXPECT_EQ(expected_same_before_, equal_before);
+
+  RTCOfferAnswerOptions options;
+  options.use_rtp_mux = (bundle_included_ == BundleIncluded::kBundleInAnswer);
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal(options)));
+  bool equal_after = (caller->voice_rtp_transport_channel() ==
+                      caller->video_rtp_transport_channel());
+  EXPECT_EQ(expected_same_after_, equal_after);
+}
+
+// The max-bundle policy means we should anticipate bundling being negotiated,
+// and multiplex audio/video from the start.
+// For all other policies, bundling should only be enabled if negotiated by the
+// answer.
+INSTANTIATE_TEST_CASE_P(
+    PeerConnectionBundleTest,
+    PeerConnectionBundleMatrixTest,
+    Values(std::make_tuple(BundlePolicy::kBundlePolicyBalanced,
+                           BundleIncluded::kBundleInAnswer,
+                           false,
+                           true),
+           std::make_tuple(BundlePolicy::kBundlePolicyBalanced,
+                           BundleIncluded::kBundleNotInAnswer,
+                           false,
+                           false),
+           std::make_tuple(BundlePolicy::kBundlePolicyMaxBundle,
+                           BundleIncluded::kBundleInAnswer,
+                           true,
+                           true),
+           std::make_tuple(BundlePolicy::kBundlePolicyMaxBundle,
+                           BundleIncluded::kBundleNotInAnswer,
+                           true,
+                           true),
+           std::make_tuple(BundlePolicy::kBundlePolicyMaxCompat,
+                           BundleIncluded::kBundleInAnswer,
+                           false,
+                           true),
+           std::make_tuple(BundlePolicy::kBundlePolicyMaxCompat,
+                           BundleIncluded::kBundleNotInAnswer,
+                           false,
+                           false)));
+
+// Test that the audio/video transports on the callee side are the same before
+// and after setting a local answer when max BUNDLE is enabled and an offer with
+// BUNDLE is received.
+TEST_F(PeerConnectionBundleTest,
+       TransportsSameForMaxBundleWithBundleInRemoteOffer) {
+  auto caller = CreatePeerConnectionWithAudioVideo();
+  RTCConfiguration config;
+  config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
+  auto callee = CreatePeerConnectionWithAudioVideo(config);
+
+  RTCOfferAnswerOptions options_with_bundle;
+  options_with_bundle.use_rtp_mux = true;
+  ASSERT_TRUE(callee->SetRemoteDescription(
+      caller->CreateOfferAndSetAsLocal(options_with_bundle)));
+
+  EXPECT_EQ(callee->voice_rtp_transport_channel(),
+            callee->video_rtp_transport_channel());
+
+  ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer()));
+
+  EXPECT_EQ(callee->voice_rtp_transport_channel(),
+            callee->video_rtp_transport_channel());
+}
+
+TEST_F(PeerConnectionBundleTest,
+       FailToSetRemoteOfferWithNoBundleWhenBundlePolicyMaxBundle) {
+  auto caller = CreatePeerConnectionWithAudioVideo();
+  RTCConfiguration config;
+  config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
+  auto callee = CreatePeerConnectionWithAudioVideo(config);
+
+  RTCOfferAnswerOptions options_no_bundle;
+  options_no_bundle.use_rtp_mux = false;
+  EXPECT_FALSE(callee->SetRemoteDescription(
+      caller->CreateOfferAndSetAsLocal(options_no_bundle)));
+}
+
+// Test that if the media section which has the bundled transport is rejected,
+// then the peers still connect and the bundled transport switches to the other
+// media section.
+// Note: This is currently failing because of the following bug:
+// https://bugs.chromium.org/p/webrtc/issues/detail?id=6280
+TEST_F(PeerConnectionBundleTest,
+       DISABLED_SuccessfullyNegotiateMaxBundleIfBundleTransportMediaRejected) {
+  RTCConfiguration config;
+  config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
+  auto caller = CreatePeerConnectionWithAudioVideo(config);
+  auto callee = CreatePeerConnection();
+  callee->AddVideoTrack("v");
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  RTCOfferAnswerOptions options;
+  options.offer_to_receive_audio = 0;
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal(options)));
+
+  EXPECT_FALSE(caller->voice_rtp_transport_channel());
+  EXPECT_TRUE(caller->video_rtp_transport_channel());
+}
+
+// When requiring RTCP multiplexing, the PeerConnection never makes RTCP
+// transport channels.
+TEST_F(PeerConnectionBundleTest, NeverCreateRtcpTransportWithRtcpMuxRequired) {
+  RTCConfiguration config;
+  config.rtcp_mux_policy = RtcpMuxPolicy::kRtcpMuxPolicyRequire;
+  auto caller = CreatePeerConnectionWithAudioVideo(config);
+  auto callee = CreatePeerConnectionWithAudioVideo();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  EXPECT_FALSE(caller->voice_rtcp_transport_channel());
+  EXPECT_FALSE(caller->video_rtcp_transport_channel());
+
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  EXPECT_FALSE(caller->voice_rtcp_transport_channel());
+  EXPECT_FALSE(caller->video_rtcp_transport_channel());
+}
+
+// When negotiating RTCP multiplexing, the PeerConnection makes RTCP transport
+// channels when the offer is sent, but will destroy them once the remote answer
+// is set.
+TEST_F(PeerConnectionBundleTest,
+       CreateRtcpTransportOnlyBeforeAnswerWithRtcpMuxNegotiate) {
+  RTCConfiguration config;
+  config.rtcp_mux_policy = RtcpMuxPolicy::kRtcpMuxPolicyNegotiate;
+  auto caller = CreatePeerConnectionWithAudioVideo(config);
+  auto callee = CreatePeerConnectionWithAudioVideo();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  EXPECT_TRUE(caller->voice_rtcp_transport_channel());
+  EXPECT_TRUE(caller->video_rtcp_transport_channel());
+
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  EXPECT_FALSE(caller->voice_rtcp_transport_channel());
+  EXPECT_FALSE(caller->video_rtcp_transport_channel());
+}
+
+TEST_F(PeerConnectionBundleTest, FailToSetDescriptionWithBundleAndNoRtcpMux) {
+  auto caller = CreatePeerConnectionWithAudioVideo();
+  auto callee = CreatePeerConnectionWithAudioVideo();
+
+  RTCOfferAnswerOptions options;
+  options.use_rtp_mux = true;
+
+  auto offer = caller->CreateOffer(options);
+  SdpContentsForEach(RemoveRtcpMux(), offer->description());
+
+  std::string error;
+  EXPECT_FALSE(caller->SetLocalDescription(CloneSessionDescription(offer.get()),
+                                           &error));
+  EXPECT_EQ(
+      "Failed to set local offer sdp: rtcp-mux must be enabled when BUNDLE is "
+      "enabled.",
+      error);
+
+  EXPECT_FALSE(callee->SetRemoteDescription(std::move(offer), &error));
+  EXPECT_EQ(
+      "Failed to set remote offer sdp: rtcp-mux must be enabled when BUNDLE is "
+      "enabled.",
+      error);
+}
+
+// Test that candidates sent to the "video" transport do not get pushed down to
+// the "audio" transport channel when bundling.
+TEST_F(PeerConnectionBundleTest,
+       IgnoreCandidatesForUnusedTransportWhenBundling) {
+  const SocketAddress kAudioAddress1("1.1.1.1", 1111);
+  const SocketAddress kAudioAddress2("2.2.2.2", 2222);
+  const SocketAddress kVideoAddress("3.3.3.3", 3333);
+  const SocketAddress kCallerAddress("4.4.4.4", 0);
+  const SocketAddress kCalleeAddress("5.5.5.5", 0);
+
+  auto caller = CreatePeerConnectionWithAudioVideo();
+  auto callee = CreatePeerConnectionWithAudioVideo();
+
+  caller->network()->AddInterface(kCallerAddress);
+  callee->network()->AddInterface(kCalleeAddress);
+
+  RTCOfferAnswerOptions options;
+  options.use_rtp_mux = true;
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  // The way the *_WAIT checks work is they only wait if the condition fails,
+  // which does not help in the case where state is not changing. This is
+  // problematic in this test since we want to verify that adding a video
+  // candidate does _not_ change state. So we interleave candidates and assume
+  // that messages are executed in the order they were posted.
+
+  cricket::Candidate audio_candidate1 = CreateLocalUdpCandidate(kAudioAddress1);
+  ASSERT_TRUE(caller->AddIceCandidateToMedia(&audio_candidate1,
+                                             cricket::MEDIA_TYPE_AUDIO));
+
+  cricket::Candidate video_candidate = CreateLocalUdpCandidate(kVideoAddress);
+  ASSERT_TRUE(caller->AddIceCandidateToMedia(&video_candidate,
+                                             cricket::MEDIA_TYPE_VIDEO));
+
+  cricket::Candidate audio_candidate2 = CreateLocalUdpCandidate(kAudioAddress2);
+  ASSERT_TRUE(caller->AddIceCandidateToMedia(&audio_candidate2,
+                                             cricket::MEDIA_TYPE_AUDIO));
+
+  EXPECT_TRUE_WAIT(caller->HasConnectionWithRemoteAddress(kAudioAddress1),
+                   kDefaultTimeout);
+  EXPECT_TRUE_WAIT(caller->HasConnectionWithRemoteAddress(kAudioAddress2),
+                   kDefaultTimeout);
+  EXPECT_FALSE(caller->HasConnectionWithRemoteAddress(kVideoAddress));
+}
+
+// Test that the transport used by both audio and video is the transport
+// associated with the first MID in the answer BUNDLE group, even if it's in a
+// different order from the offer.
+TEST_F(PeerConnectionBundleTest, BundleOnFirstMidInAnswer) {
+  auto caller = CreatePeerConnectionWithAudioVideo();
+  auto callee = CreatePeerConnectionWithAudioVideo();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto* old_video_transport = caller->video_rtp_transport_channel();
+
+  auto answer = callee->CreateAnswer();
+  auto* old_bundle_group =
+      answer->description()->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  ASSERT_THAT(old_bundle_group->content_names(),
+              ElementsAre(cricket::CN_AUDIO, cricket::CN_VIDEO));
+  answer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+
+  cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  new_bundle_group.AddContentName(cricket::CN_VIDEO);
+  new_bundle_group.AddContentName(cricket::CN_AUDIO);
+  answer->description()->AddGroup(new_bundle_group);
+
+  ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+
+  EXPECT_EQ(old_video_transport, caller->video_rtp_transport_channel());
+  EXPECT_EQ(caller->voice_rtp_transport_channel(),
+            caller->video_rtp_transport_channel());
+}
+
+}  // namespace webrtc