diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc')
-rw-r--r-- | third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc | 2704 |
1 files changed, 2704 insertions, 0 deletions
diff --git a/third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc b/third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc new file mode 100644 index 0000000000..c30f381971 --- /dev/null +++ b/third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc @@ -0,0 +1,2704 @@ +/* + * Copyright 2018 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 "pc/jsep_transport_controller.h" + +#include <map> +#include <string> +#include <utility> + +#include "api/dtls_transport_interface.h" +#include "api/transport/enums.h" +#include "p2p/base/candidate_pair_interface.h" +#include "p2p/base/dtls_transport_factory.h" +#include "p2p/base/fake_dtls_transport.h" +#include "p2p/base/fake_ice_transport.h" +#include "p2p/base/p2p_constants.h" +#include "p2p/base/transport_info.h" +#include "rtc_base/fake_ssl_identity.h" +#include "rtc_base/gunit.h" +#include "rtc_base/logging.h" +#include "rtc_base/net_helper.h" +#include "rtc_base/socket_address.h" +#include "rtc_base/ssl_fingerprint.h" +#include "rtc_base/ssl_identity.h" +#include "rtc_base/task_queue_for_test.h" +#include "rtc_base/thread.h" +#include "test/gtest.h" +#include "test/scoped_key_value_config.h" + +using cricket::Candidate; +using cricket::Candidates; +using cricket::FakeDtlsTransport; +using webrtc::SdpType; + +static const int kTimeout = 100; +static const char kIceUfrag1[] = "u0001"; +static const char kIcePwd1[] = "TESTICEPWD00000000000001"; +static const char kIceUfrag2[] = "u0002"; +static const char kIcePwd2[] = "TESTICEPWD00000000000002"; +static const char kIceUfrag3[] = "u0003"; +static const char kIcePwd3[] = "TESTICEPWD00000000000003"; +static const char kIceUfrag4[] = "u0004"; +static const char kIcePwd4[] = "TESTICEPWD00000000000004"; +static const char kAudioMid1[] = "audio1"; +static const char kAudioMid2[] = "audio2"; +static const char kVideoMid1[] = "video1"; +static const char kVideoMid2[] = "video2"; +static const char kDataMid1[] = "data1"; + +namespace webrtc { + +class FakeIceTransportFactory : public webrtc::IceTransportFactory { + public: + ~FakeIceTransportFactory() override = default; + rtc::scoped_refptr<IceTransportInterface> CreateIceTransport( + const std::string& transport_name, + int component, + IceTransportInit init) override { + return rtc::make_ref_counted<cricket::FakeIceTransportWrapper>( + std::make_unique<cricket::FakeIceTransport>(transport_name, component)); + } +}; + +class FakeDtlsTransportFactory : public cricket::DtlsTransportFactory { + public: + std::unique_ptr<cricket::DtlsTransportInternal> CreateDtlsTransport( + cricket::IceTransportInternal* ice, + const webrtc::CryptoOptions& crypto_options, + rtc::SSLProtocolVersion max_version) override { + return std::make_unique<FakeDtlsTransport>( + static_cast<cricket::FakeIceTransport*>(ice)); + } +}; + +class JsepTransportControllerTest : public JsepTransportController::Observer, + public ::testing::Test, + public sigslot::has_slots<> { + public: + JsepTransportControllerTest() : signaling_thread_(rtc::Thread::Current()) { + fake_ice_transport_factory_ = std::make_unique<FakeIceTransportFactory>(); + fake_dtls_transport_factory_ = std::make_unique<FakeDtlsTransportFactory>(); + } + + void CreateJsepTransportController( + JsepTransportController::Config config, + rtc::Thread* network_thread = rtc::Thread::Current(), + cricket::PortAllocator* port_allocator = nullptr) { + config.transport_observer = this; + config.rtcp_handler = [](const rtc::CopyOnWriteBuffer& packet, + int64_t packet_time_us) { + RTC_DCHECK_NOTREACHED(); + }; + config.ice_transport_factory = fake_ice_transport_factory_.get(); + config.dtls_transport_factory = fake_dtls_transport_factory_.get(); + config.on_dtls_handshake_error_ = [](rtc::SSLHandshakeError s) {}; + config.field_trials = &field_trials_; + transport_controller_ = std::make_unique<JsepTransportController>( + network_thread, port_allocator, nullptr /* async_resolver_factory */, + config); + SendTask(network_thread, [&] { ConnectTransportControllerSignals(); }); + } + + void ConnectTransportControllerSignals() { + transport_controller_->SubscribeIceConnectionState( + [this](cricket::IceConnectionState s) { + JsepTransportControllerTest::OnConnectionState(s); + }); + transport_controller_->SubscribeConnectionState( + [this](PeerConnectionInterface::PeerConnectionState s) { + JsepTransportControllerTest::OnCombinedConnectionState(s); + }); + transport_controller_->SubscribeStandardizedIceConnectionState( + [this](PeerConnectionInterface::IceConnectionState s) { + JsepTransportControllerTest::OnStandardizedIceConnectionState(s); + }); + transport_controller_->SubscribeIceGatheringState( + [this](cricket::IceGatheringState s) { + JsepTransportControllerTest::OnGatheringState(s); + }); + transport_controller_->SubscribeIceCandidateGathered( + [this](const std::string& transport, + const std::vector<cricket::Candidate>& candidates) { + JsepTransportControllerTest::OnCandidatesGathered(transport, + candidates); + }); + } + + std::unique_ptr<cricket::SessionDescription> + CreateSessionDescriptionWithoutBundle() { + auto description = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(description.get(), kVideoMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + return description; + } + + std::unique_ptr<cricket::SessionDescription> + CreateSessionDescriptionWithBundleGroup() { + auto description = CreateSessionDescriptionWithoutBundle(); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + description->AddGroup(bundle_group); + + return description; + } + + std::unique_ptr<cricket::SessionDescription> + CreateSessionDescriptionWithBundledData() { + auto description = CreateSessionDescriptionWithoutBundle(); + AddDataSection(description.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + bundle_group.AddContentName(kDataMid1); + description->AddGroup(bundle_group); + return description; + } + + void AddAudioSection(cricket::SessionDescription* description, + const std::string& mid, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr<rtc::RTCCertificate> cert) { + std::unique_ptr<cricket::AudioContentDescription> audio( + new cricket::AudioContentDescription()); + // Set RTCP-mux to be true because the default policy is "mux required". + audio->set_rtcp_mux(true); + description->AddContent(mid, cricket::MediaProtocolType::kRtp, + /*rejected=*/false, std::move(audio)); + AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); + } + + void AddVideoSection(cricket::SessionDescription* description, + const std::string& mid, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr<rtc::RTCCertificate> cert) { + std::unique_ptr<cricket::VideoContentDescription> video( + new cricket::VideoContentDescription()); + // Set RTCP-mux to be true because the default policy is "mux required". + video->set_rtcp_mux(true); + description->AddContent(mid, cricket::MediaProtocolType::kRtp, + /*rejected=*/false, std::move(video)); + AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); + } + + void AddDataSection(cricket::SessionDescription* description, + const std::string& mid, + cricket::MediaProtocolType protocol_type, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr<rtc::RTCCertificate> cert) { + RTC_CHECK(protocol_type == cricket::MediaProtocolType::kSctp); + std::unique_ptr<cricket::SctpDataContentDescription> data( + new cricket::SctpDataContentDescription()); + data->set_rtcp_mux(true); + description->AddContent(mid, protocol_type, + /*rejected=*/false, std::move(data)); + AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); + } + + void AddTransportInfo(cricket::SessionDescription* description, + const std::string& mid, + const std::string& ufrag, + const std::string& pwd, + cricket::IceMode ice_mode, + cricket::ConnectionRole conn_role, + rtc::scoped_refptr<rtc::RTCCertificate> cert) { + std::unique_ptr<rtc::SSLFingerprint> fingerprint; + if (cert) { + fingerprint = rtc::SSLFingerprint::CreateFromCertificate(*cert); + } + + cricket::TransportDescription transport_desc(std::vector<std::string>(), + ufrag, pwd, ice_mode, + conn_role, fingerprint.get()); + description->AddTransportInfo(cricket::TransportInfo(mid, transport_desc)); + } + + cricket::IceConfig CreateIceConfig( + int receiving_timeout, + cricket::ContinualGatheringPolicy continual_gathering_policy) { + cricket::IceConfig config; + config.receiving_timeout = receiving_timeout; + config.continual_gathering_policy = continual_gathering_policy; + return config; + } + + Candidate CreateCandidate(const std::string& transport_name, int component) { + Candidate c; + c.set_transport_name(transport_name); + c.set_address(rtc::SocketAddress("192.168.1.1", 8000)); + c.set_component(component); + c.set_protocol(cricket::UDP_PROTOCOL_NAME); + c.set_priority(1); + return c; + } + + void CreateLocalDescriptionAndCompleteConnectionOnNetworkThread() { + if (!network_thread_->IsCurrent()) { + SendTask(network_thread_.get(), [&] { + CreateLocalDescriptionAndCompleteConnectionOnNetworkThread(); + }); + return; + } + + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + transport_controller_->MaybeStartGathering(); + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kVideoMid1)); + fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered( + fake_audio_dtls->fake_ice_transport(), + CreateCandidate(kAudioMid1, /*component=*/1)); + fake_video_dtls->fake_ice_transport()->SignalCandidateGathered( + fake_video_dtls->fake_ice_transport(), + CreateCandidate(kVideoMid1, /*component=*/1)); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(2); + fake_video_dtls->fake_ice_transport()->SetConnectionCount(2); + fake_audio_dtls->SetReceiving(true); + fake_video_dtls->SetReceiving(true); + fake_audio_dtls->SetWritable(true); + fake_video_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_video_dtls->fake_ice_transport()->SetConnectionCount(1); + } + + protected: + void OnConnectionState(cricket::IceConnectionState state) { + ice_signaled_on_thread_ = rtc::Thread::Current(); + connection_state_ = state; + ++connection_state_signal_count_; + } + + void OnStandardizedIceConnectionState( + PeerConnectionInterface::IceConnectionState state) { + ice_signaled_on_thread_ = rtc::Thread::Current(); + ice_connection_state_ = state; + ++ice_connection_state_signal_count_; + } + + void OnCombinedConnectionState( + PeerConnectionInterface::PeerConnectionState state) { + RTC_LOG(LS_INFO) << "OnCombinedConnectionState: " + << static_cast<int>(state); + ice_signaled_on_thread_ = rtc::Thread::Current(); + combined_connection_state_ = state; + ++combined_connection_state_signal_count_; + } + + void OnGatheringState(cricket::IceGatheringState state) { + ice_signaled_on_thread_ = rtc::Thread::Current(); + gathering_state_ = state; + ++gathering_state_signal_count_; + } + + void OnCandidatesGathered(const std::string& transport_name, + const Candidates& candidates) { + ice_signaled_on_thread_ = rtc::Thread::Current(); + candidates_[transport_name].insert(candidates_[transport_name].end(), + candidates.begin(), candidates.end()); + ++candidates_signal_count_; + } + + // JsepTransportController::Observer overrides. + bool OnTransportChanged( + const std::string& mid, + RtpTransportInternal* rtp_transport, + rtc::scoped_refptr<DtlsTransport> dtls_transport, + DataChannelTransportInterface* data_channel_transport) override { + changed_rtp_transport_by_mid_[mid] = rtp_transport; + if (dtls_transport) { + changed_dtls_transport_by_mid_[mid] = dtls_transport->internal(); + } else { + changed_dtls_transport_by_mid_[mid] = nullptr; + } + return true; + } + + rtc::AutoThread main_thread_; + // Information received from signals from transport controller. + cricket::IceConnectionState connection_state_ = + cricket::kIceConnectionConnecting; + PeerConnectionInterface::IceConnectionState ice_connection_state_ = + PeerConnectionInterface::kIceConnectionNew; + PeerConnectionInterface::PeerConnectionState combined_connection_state_ = + PeerConnectionInterface::PeerConnectionState::kNew; + bool receiving_ = false; + cricket::IceGatheringState gathering_state_ = cricket::kIceGatheringNew; + // transport_name => candidates + std::map<std::string, Candidates> candidates_; + // Counts of each signal emitted. + int connection_state_signal_count_ = 0; + int ice_connection_state_signal_count_ = 0; + int combined_connection_state_signal_count_ = 0; + int receiving_signal_count_ = 0; + int gathering_state_signal_count_ = 0; + int candidates_signal_count_ = 0; + + // `network_thread_` should be destroyed after `transport_controller_` + std::unique_ptr<rtc::Thread> network_thread_; + std::unique_ptr<FakeIceTransportFactory> fake_ice_transport_factory_; + std::unique_ptr<FakeDtlsTransportFactory> fake_dtls_transport_factory_; + rtc::Thread* const signaling_thread_ = nullptr; + rtc::Thread* ice_signaled_on_thread_ = nullptr; + // Used to verify the SignalRtpTransportChanged/SignalDtlsTransportChanged are + // signaled correctly. + std::map<std::string, RtpTransportInternal*> changed_rtp_transport_by_mid_; + std::map<std::string, cricket::DtlsTransportInternal*> + changed_dtls_transport_by_mid_; + + // Transport controller needs to be destroyed first, because it may issue + // callbacks that modify the changed_*_by_mid in the destructor. + std::unique_ptr<JsepTransportController> transport_controller_; + webrtc::test::ScopedKeyValueConfig field_trials_; +}; + +TEST_F(JsepTransportControllerTest, GetRtpTransport) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + auto audio_rtp_transport = transport_controller_->GetRtpTransport(kAudioMid1); + auto video_rtp_transport = transport_controller_->GetRtpTransport(kVideoMid1); + EXPECT_NE(nullptr, audio_rtp_transport); + EXPECT_NE(nullptr, video_rtp_transport); + EXPECT_NE(audio_rtp_transport, video_rtp_transport); + // Return nullptr for non-existing ones. + EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kAudioMid2)); +} + +TEST_F(JsepTransportControllerTest, GetDtlsTransport) { + JsepTransportController::Config config; + config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyNegotiate; + CreateJsepTransportController(config); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_NE(nullptr, transport_controller_->GetRtcpDtlsTransport(kAudioMid1)); + EXPECT_NE(nullptr, + transport_controller_->LookupDtlsTransportByMid(kAudioMid1)); + EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kVideoMid1)); + EXPECT_NE(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid1)); + EXPECT_NE(nullptr, + transport_controller_->LookupDtlsTransportByMid(kVideoMid1)); + // Lookup for all MIDs should return different transports (no bundle) + EXPECT_NE(transport_controller_->LookupDtlsTransportByMid(kAudioMid1), + transport_controller_->LookupDtlsTransportByMid(kVideoMid1)); + // Return nullptr for non-existing ones. + EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kVideoMid2)); + EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid2)); + EXPECT_EQ(nullptr, + transport_controller_->LookupDtlsTransportByMid(kVideoMid2)); + // Take a pointer to a transport, shut down the transport controller, + // and verify that the resulting container is empty. + auto dtls_transport = + transport_controller_->LookupDtlsTransportByMid(kVideoMid1); + webrtc::DtlsTransport* my_transport = + static_cast<DtlsTransport*>(dtls_transport.get()); + EXPECT_NE(nullptr, my_transport->internal()); + transport_controller_.reset(); + EXPECT_EQ(nullptr, my_transport->internal()); +} + +TEST_F(JsepTransportControllerTest, GetDtlsTransportWithRtcpMux) { + JsepTransportController::Config config; + config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyRequire; + CreateJsepTransportController(config); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kAudioMid1)); + EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kVideoMid1)); + EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid1)); +} + +TEST_F(JsepTransportControllerTest, SetIceConfig) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + transport_controller_->SetIceConfig( + CreateIceConfig(kTimeout, cricket::GATHER_CONTINUALLY)); + FakeDtlsTransport* fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + ASSERT_NE(nullptr, fake_audio_dtls); + EXPECT_EQ(kTimeout, + fake_audio_dtls->fake_ice_transport()->receiving_timeout()); + EXPECT_TRUE(fake_audio_dtls->fake_ice_transport()->gather_continually()); + + // Test that value stored in controller is applied to new transports. + AddAudioSection(description.get(), kAudioMid2, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid2)); + ASSERT_NE(nullptr, fake_audio_dtls); + EXPECT_EQ(kTimeout, + fake_audio_dtls->fake_ice_transport()->receiving_timeout()); + EXPECT_TRUE(fake_audio_dtls->fake_ice_transport()->gather_continually()); +} + +// Tests the getter and setter of the ICE restart flag. +TEST_F(JsepTransportControllerTest, NeedIceRestart) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, description.get()) + .ok()); + + // Initially NeedsIceRestart should return false. + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kAudioMid1)); + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kVideoMid1)); + // Set the needs-ice-restart flag and verify NeedsIceRestart starts returning + // true. + transport_controller_->SetNeedsIceRestartFlag(); + EXPECT_TRUE(transport_controller_->NeedsIceRestart(kAudioMid1)); + EXPECT_TRUE(transport_controller_->NeedsIceRestart(kVideoMid1)); + // For a nonexistent transport, false should be returned. + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kVideoMid2)); + + // Reset the ice_ufrag/ice_pwd for audio. + auto audio_transport_info = description->GetTransportInfoByName(kAudioMid1); + audio_transport_info->description.ice_ufrag = kIceUfrag2; + audio_transport_info->description.ice_pwd = kIcePwd2; + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + // Because the ICE is only restarted for audio, NeedsIceRestart is expected to + // return false for audio and true for video. + EXPECT_FALSE(transport_controller_->NeedsIceRestart(kAudioMid1)); + EXPECT_TRUE(transport_controller_->NeedsIceRestart(kVideoMid1)); +} + +TEST_F(JsepTransportControllerTest, MaybeStartGathering) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + // After setting the local description, we should be able to start gathering + // candidates. + transport_controller_->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, AddRemoveRemoteCandidates) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + transport_controller_->SetLocalDescription(SdpType::kOffer, + description.get()); + transport_controller_->SetRemoteDescription(SdpType::kAnswer, + description.get()); + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + ASSERT_NE(nullptr, fake_audio_dtls); + Candidates candidates; + candidates.push_back( + CreateCandidate(kAudioMid1, cricket::ICE_CANDIDATE_COMPONENT_RTP)); + EXPECT_TRUE( + transport_controller_->AddRemoteCandidates(kAudioMid1, candidates).ok()); + EXPECT_EQ(1U, + fake_audio_dtls->fake_ice_transport()->remote_candidates().size()); + + EXPECT_TRUE(transport_controller_->RemoveRemoteCandidates(candidates).ok()); + EXPECT_EQ(0U, + fake_audio_dtls->fake_ice_transport()->remote_candidates().size()); +} + +TEST_F(JsepTransportControllerTest, SetAndGetLocalCertificate) { + CreateJsepTransportController(JsepTransportController::Config()); + + rtc::scoped_refptr<rtc::RTCCertificate> certificate1 = + rtc::RTCCertificate::Create( + rtc::SSLIdentity::Create("session1", rtc::KT_DEFAULT)); + rtc::scoped_refptr<rtc::RTCCertificate> returned_certificate; + + auto description = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + certificate1); + + // Apply the local certificate. + EXPECT_TRUE(transport_controller_->SetLocalCertificate(certificate1)); + // Apply the local description. + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + returned_certificate = transport_controller_->GetLocalCertificate(kAudioMid1); + EXPECT_TRUE(returned_certificate); + EXPECT_EQ(certificate1->identity()->certificate().ToPEMString(), + returned_certificate->identity()->certificate().ToPEMString()); + + // Should fail if called for a nonexistant transport. + EXPECT_EQ(nullptr, transport_controller_->GetLocalCertificate(kVideoMid1)); + + // Shouldn't be able to change the identity once set. + rtc::scoped_refptr<rtc::RTCCertificate> certificate2 = + rtc::RTCCertificate::Create( + rtc::SSLIdentity::Create("session2", rtc::KT_DEFAULT)); + EXPECT_FALSE(transport_controller_->SetLocalCertificate(certificate2)); +} + +TEST_F(JsepTransportControllerTest, GetRemoteSSLCertChain) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + rtc::FakeSSLCertificate fake_certificate("fake_data"); + + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + fake_audio_dtls->SetRemoteSSLCertificate(&fake_certificate); + std::unique_ptr<rtc::SSLCertChain> returned_cert_chain = + transport_controller_->GetRemoteSSLCertChain(kAudioMid1); + ASSERT_TRUE(returned_cert_chain); + ASSERT_EQ(1u, returned_cert_chain->GetSize()); + EXPECT_EQ(fake_certificate.ToPEMString(), + returned_cert_chain->Get(0).ToPEMString()); + + // Should fail if called for a nonexistant transport. + EXPECT_FALSE(transport_controller_->GetRemoteSSLCertChain(kAudioMid2)); +} + +TEST_F(JsepTransportControllerTest, GetDtlsRole) { + CreateJsepTransportController(JsepTransportController::Config()); + auto offer_certificate = rtc::RTCCertificate::Create( + rtc::SSLIdentity::Create("offer", rtc::KT_DEFAULT)); + auto answer_certificate = rtc::RTCCertificate::Create( + rtc::SSLIdentity::Create("answer", rtc::KT_DEFAULT)); + transport_controller_->SetLocalCertificate(offer_certificate); + + auto offer_desc = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(offer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + offer_certificate); + auto answer_desc = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(answer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + answer_certificate); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, offer_desc.get()) + .ok()); + + absl::optional<rtc::SSLRole> role = + transport_controller_->GetDtlsRole(kAudioMid1); + // The DTLS role is not decided yet. + EXPECT_FALSE(role); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, answer_desc.get()) + .ok()); + role = transport_controller_->GetDtlsRole(kAudioMid1); + + ASSERT_TRUE(role); + EXPECT_EQ(rtc::SSL_CLIENT, *role); +} + +TEST_F(JsepTransportControllerTest, GetStats) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + cricket::TransportStats stats; + EXPECT_TRUE(transport_controller_->GetStats(kAudioMid1, &stats)); + EXPECT_EQ(kAudioMid1, stats.transport_name); + EXPECT_EQ(1u, stats.channel_stats.size()); + // Return false for non-existing transport. + EXPECT_FALSE(transport_controller_->GetStats(kAudioMid2, &stats)); +} + +TEST_F(JsepTransportControllerTest, SignalConnectionStateFailed) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_ice = static_cast<cricket::FakeIceTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)->ice_transport()); + fake_ice->SetCandidatesGatheringComplete(); + fake_ice->SetConnectionCount(1); + // The connection stats will be failed if there is no active connection. + fake_ice->SetConnectionCount(0); + EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout); + EXPECT_EQ(1, connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionFailed, + ice_connection_state_, kTimeout); + EXPECT_EQ(1, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kFailed, + combined_connection_state_, kTimeout); + EXPECT_EQ(1, combined_connection_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, + SignalConnectionStateConnectedNoMediaTransport) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kVideoMid1)); + + // First, have one transport connect, and another fail, to ensure that + // the first transport connecting didn't trigger a "connected" state signal. + // We should only get a signal when all are connected. + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_audio_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + // Decrease the number of the connection to trigger the signal. + fake_video_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_video_dtls->fake_ice_transport()->SetConnectionCount(0); + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + + EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout); + EXPECT_EQ(1, connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionFailed, + ice_connection_state_, kTimeout); + EXPECT_EQ(2, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kFailed, + combined_connection_state_, kTimeout); + EXPECT_EQ(2, combined_connection_state_signal_count_); + + fake_audio_dtls->SetDtlsState(DtlsTransportState::kConnected); + fake_video_dtls->SetDtlsState(DtlsTransportState::kConnected); + // Set the connection count to be 2 and the cricket::FakeIceTransport will set + // the transport state to be STATE_CONNECTING. + fake_video_dtls->fake_ice_transport()->SetConnectionCount(2); + fake_video_dtls->SetWritable(true); + EXPECT_EQ_WAIT(cricket::kIceConnectionConnected, connection_state_, kTimeout); + EXPECT_EQ(2, connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionConnected, + ice_connection_state_, kTimeout); + EXPECT_EQ(3, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnected, + combined_connection_state_, kTimeout); + EXPECT_EQ(3, combined_connection_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalConnectionStateComplete) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kVideoMid1)); + + // First, have one transport connect, and another fail, to ensure that + // the first transport connecting didn't trigger a "connected" state signal. + // We should only get a signal when all are connected. + fake_audio_dtls->fake_ice_transport()->SetTransportState( + IceTransportState::kCompleted, + cricket::IceTransportState::STATE_COMPLETED); + fake_audio_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking, + ice_connection_state_, kTimeout); + EXPECT_EQ(1, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting, + combined_connection_state_, kTimeout); + EXPECT_EQ(1, combined_connection_state_signal_count_); + + fake_video_dtls->fake_ice_transport()->SetTransportState( + IceTransportState::kFailed, cricket::IceTransportState::STATE_FAILED); + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + + EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout); + EXPECT_EQ(1, connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionFailed, + ice_connection_state_, kTimeout); + EXPECT_EQ(2, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kFailed, + combined_connection_state_, kTimeout); + EXPECT_EQ(2, combined_connection_state_signal_count_); + + fake_audio_dtls->SetDtlsState(DtlsTransportState::kConnected); + fake_video_dtls->SetDtlsState(DtlsTransportState::kConnected); + // Set the connection count to be 1 and the cricket::FakeIceTransport will set + // the transport state to be STATE_COMPLETED. + fake_video_dtls->fake_ice_transport()->SetTransportState( + IceTransportState::kCompleted, + cricket::IceTransportState::STATE_COMPLETED); + fake_video_dtls->SetWritable(true); + EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout); + EXPECT_EQ(3, connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionCompleted, + ice_connection_state_, kTimeout); + EXPECT_EQ(3, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnected, + combined_connection_state_, kTimeout); + EXPECT_EQ(3, combined_connection_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalIceGatheringStateGathering) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); + // Should be in the gathering state as soon as any transport starts gathering. + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalIceGatheringStateComplete) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithoutBundle(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kVideoMid1)); + + fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); + + // Have one transport finish gathering, to make sure gathering + // completion wasn't signalled if only one transport finished gathering. + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + EXPECT_EQ(1, gathering_state_signal_count_); + + fake_video_dtls->fake_ice_transport()->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); + + fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout); + EXPECT_EQ(2, gathering_state_signal_count_); +} + +// Test that when the last transport that hasn't finished connecting and/or +// gathering is destroyed, the aggregate state jumps to "completed". This can +// happen if, for example, we have an audio and video transport, the audio +// transport completes, then we start bundling video on the audio transport. +TEST_F(JsepTransportControllerTest, + SignalingWhenLastIncompleteTransportDestroyed) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + auto fake_video_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kVideoMid1)); + EXPECT_NE(fake_audio_dtls, fake_video_dtls); + + fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); + + // Let the audio transport complete. + fake_audio_dtls->SetWritable(true); + fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); + fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1); + fake_audio_dtls->SetDtlsState(DtlsTransportState::kConnected); + EXPECT_EQ(1, gathering_state_signal_count_); + + // Set the remote description and enable the bundle. + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, description.get()) + .ok()); + // The BUNDLE should be enabled, the incomplete video transport should be + // deleted and the states shoud be updated. + fake_video_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kVideoMid1)); + EXPECT_EQ(fake_audio_dtls, fake_video_dtls); + EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout); + EXPECT_EQ(PeerConnectionInterface::kIceConnectionCompleted, + ice_connection_state_); + EXPECT_EQ(PeerConnectionInterface::PeerConnectionState::kConnected, + combined_connection_state_); + EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout); + EXPECT_EQ(2, gathering_state_signal_count_); +} + +// Test that states immediately return to "new" if all transports are +// discarded. This should happen at offer time, even though the transport +// controller may keep the transport alive in case of rollback. +TEST_F(JsepTransportControllerTest, + IceStatesReturnToNewWhenTransportsDiscarded) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, description.get()) + .ok()); + + // Trigger and verify initial non-new states. + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); + fake_audio_dtls->fake_ice_transport()->SetTransportState( + webrtc::IceTransportState::kChecking, + cricket::IceTransportState::STATE_CONNECTING); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking, + ice_connection_state_, kTimeout); + EXPECT_EQ(1, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting, + combined_connection_state_, kTimeout); + EXPECT_EQ(1, combined_connection_state_signal_count_); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(1, gathering_state_signal_count_); + + // Reject m= section which should disconnect the transport and return states + // to "new". + description->contents()[0].rejected = true; + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, description.get()) + .ok()); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionNew, + ice_connection_state_, kTimeout); + EXPECT_EQ(2, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kNew, + combined_connection_state_, kTimeout); + EXPECT_EQ(2, combined_connection_state_signal_count_); + EXPECT_EQ_WAIT(cricket::kIceGatheringNew, gathering_state_, kTimeout); + EXPECT_EQ(2, gathering_state_signal_count_); + + // For good measure, rollback the offer and verify that states return to + // their previous values. + EXPECT_TRUE(transport_controller_->RollbackTransports().ok()); + EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking, + ice_connection_state_, kTimeout); + EXPECT_EQ(3, ice_connection_state_signal_count_); + EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting, + combined_connection_state_, kTimeout); + EXPECT_EQ(3, combined_connection_state_signal_count_); + EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout); + EXPECT_EQ(3, gathering_state_signal_count_); +} + +TEST_F(JsepTransportControllerTest, SignalCandidatesGathered) { + CreateJsepTransportController(JsepTransportController::Config()); + auto description = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, description.get()) + .ok()); + transport_controller_->MaybeStartGathering(); + + auto fake_audio_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered( + fake_audio_dtls->fake_ice_transport(), CreateCandidate(kAudioMid1, 1)); + EXPECT_EQ_WAIT(1, candidates_signal_count_, kTimeout); + EXPECT_EQ(1u, candidates_[kAudioMid1].size()); +} + +TEST_F(JsepTransportControllerTest, IceSignalingOccursOnNetworkThread) { + network_thread_ = rtc::Thread::CreateWithSocketServer(); + network_thread_->Start(); + EXPECT_EQ(ice_signaled_on_thread_, nullptr); + CreateJsepTransportController(JsepTransportController::Config(), + network_thread_.get(), + /*port_allocator=*/nullptr); + CreateLocalDescriptionAndCompleteConnectionOnNetworkThread(); + + // connecting --> connected --> completed + EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout); + EXPECT_EQ(2, connection_state_signal_count_); + + // new --> gathering --> complete + EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout); + EXPECT_EQ(2, gathering_state_signal_count_); + + EXPECT_EQ_WAIT(1u, candidates_[kAudioMid1].size(), kTimeout); + EXPECT_EQ_WAIT(1u, candidates_[kVideoMid1].size(), kTimeout); + EXPECT_EQ(2, candidates_signal_count_); + + EXPECT_EQ(ice_signaled_on_thread_, network_thread_.get()); + + SendTask(network_thread_.get(), [&] { transport_controller_.reset(); }); +} + +// Test that if the TransportController was created with the +// `redetermine_role_on_ice_restart` parameter set to false, the role is *not* +// redetermined on an ICE restart. +TEST_F(JsepTransportControllerTest, IceRoleNotRedetermined) { + JsepTransportController::Config config; + config.redetermine_role_on_ice_restart = false; + + CreateJsepTransportController(config); + // Let the `transport_controller_` be the controlled side initially. + auto remote_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto local_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, remote_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kAnswer, local_answer.get()) + .ok()); + + auto fake_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(cricket::ICEROLE_CONTROLLED, + fake_dtls->fake_ice_transport()->GetIceRole()); + + // New offer will trigger the ICE restart. + auto restart_local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(restart_local_offer.get(), kAudioMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + EXPECT_TRUE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, restart_local_offer.get()) + .ok()); + EXPECT_EQ(cricket::ICEROLE_CONTROLLED, + fake_dtls->fake_ice_transport()->GetIceRole()); +} + +// Tests ICE-Lite mode in remote answer. +TEST_F(JsepTransportControllerTest, SetIceRoleWhenIceLiteInRemoteAnswer) { + CreateJsepTransportController(JsepTransportController::Config()); + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + auto fake_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); + EXPECT_EQ(cricket::ICEMODE_FULL, + fake_dtls->fake_ice_transport()->remote_ice_mode()); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); + EXPECT_EQ(cricket::ICEMODE_LITE, + fake_dtls->fake_ice_transport()->remote_ice_mode()); +} + +// Tests that the ICE role remains "controlling" if a subsequent offer that +// does an ICE restart is received from an ICE lite endpoint. Regression test +// for: https://crbug.com/710760 +TEST_F(JsepTransportControllerTest, + IceRoleIsControllingAfterIceRestartFromIceLiteEndpoint) { + CreateJsepTransportController(JsepTransportController::Config()); + auto remote_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto local_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + // Initial Offer/Answer exchange. If the remote offerer is ICE-Lite, then the + // local side is the controlling. + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, remote_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kAnswer, local_answer.get()) + .ok()); + auto fake_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); + + // In the subsequence remote offer triggers an ICE restart. + auto remote_offer2 = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_offer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto local_answer2 = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_answer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kOffer, remote_offer2.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kAnswer, local_answer2.get()) + .ok()); + fake_dtls = static_cast<FakeDtlsTransport*>( + transport_controller_->GetDtlsTransport(kAudioMid1)); + // The local side is still the controlling role since the remote side is using + // ICE-Lite. + EXPECT_EQ(cricket::ICEROLE_CONTROLLING, + fake_dtls->fake_ice_transport()->GetIceRole()); +} + +// Tests that the SDP has more than one audio/video m= sections. +TEST_F(JsepTransportControllerTest, MultipleMediaSectionsOfSameTypeWithBundle) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kAudioMid2); + bundle_group.AddContentName(kVideoMid1); + bundle_group.AddContentName(kDataMid1); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddDataSection(local_offer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddDataSection(remote_answer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + // Verify that all the sections are bundled on kAudio1. + auto transport1 = transport_controller_->GetRtpTransport(kAudioMid1); + auto transport2 = transport_controller_->GetRtpTransport(kAudioMid2); + auto transport3 = transport_controller_->GetRtpTransport(kVideoMid1); + auto transport4 = transport_controller_->GetRtpTransport(kDataMid1); + EXPECT_EQ(transport1, transport2); + EXPECT_EQ(transport1, transport3); + EXPECT_EQ(transport1, transport4); + + EXPECT_EQ(transport_controller_->LookupDtlsTransportByMid(kAudioMid1), + transport_controller_->LookupDtlsTransportByMid(kVideoMid1)); + + // Verify the OnRtpTransport/DtlsTransportChanged signals are fired correctly. + auto it = changed_rtp_transport_by_mid_.find(kAudioMid2); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + it = changed_rtp_transport_by_mid_.find(kAudioMid2); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + it = changed_rtp_transport_by_mid_.find(kVideoMid1); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + // Verify the DtlsTransport for the SCTP data channel is reset correctly. + auto it2 = changed_dtls_transport_by_mid_.find(kDataMid1); + ASSERT_TRUE(it2 != changed_dtls_transport_by_mid_.end()); +} + +TEST_F(JsepTransportControllerTest, MultipleBundleGroups) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Video[] = "2_video"; + static const char kMid3Audio[] = "3_audio"; + static const char kMid4Video[] = "4_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE); + bundle_group1.AddContentName(kMid1Audio); + bundle_group1.AddContentName(kMid2Video); + cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE); + bundle_group2.AddContentName(kMid3Audio); + bundle_group2.AddContentName(kMid4Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(bundle_group1); + local_offer->AddGroup(bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(bundle_group1); + remote_answer->AddGroup(bundle_group2); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Verify that (kMid1Audio,kMid2Video) and (kMid3Audio,kMid4Video) form two + // distinct bundled groups. + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Video); + auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video); + EXPECT_EQ(mid1_transport, mid2_transport); + EXPECT_EQ(mid3_transport, mid4_transport); + EXPECT_NE(mid1_transport, mid3_transport); + + auto it = changed_rtp_transport_by_mid_.find(kMid1Audio); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(it->second, mid1_transport); + + it = changed_rtp_transport_by_mid_.find(kMid2Video); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(it->second, mid2_transport); + + it = changed_rtp_transport_by_mid_.find(kMid3Audio); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(it->second, mid3_transport); + + it = changed_rtp_transport_by_mid_.find(kMid4Video); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(it->second, mid4_transport); +} + +TEST_F(JsepTransportControllerTest, + MultipleBundleGroupsInOfferButOnlyASingleGroupInAnswer) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Video[] = "2_video"; + static const char kMid3Audio[] = "3_audio"; + static const char kMid4Video[] = "4_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE); + bundle_group1.AddContentName(kMid1Audio); + bundle_group1.AddContentName(kMid2Video); + cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE); + bundle_group2.AddContentName(kMid3Audio); + bundle_group2.AddContentName(kMid4Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + // The offer has both groups. + local_offer->AddGroup(bundle_group1); + local_offer->AddGroup(bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + // The answer only has a single group! This is what happens when talking to an + // endpoint that does not have support for multiple BUNDLE groups. + remote_answer->AddGroup(bundle_group1); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Verify that (kMid1Audio,kMid2Video) form a bundle group, but that + // kMid3Audio and kMid4Video are unbundled. + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Video); + auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video); + EXPECT_EQ(mid1_transport, mid2_transport); + EXPECT_NE(mid3_transport, mid4_transport); + EXPECT_NE(mid1_transport, mid3_transport); + EXPECT_NE(mid1_transport, mid4_transport); +} + +TEST_F(JsepTransportControllerTest, MultipleBundleGroupsIllegallyChangeGroup) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Video[] = "2_video"; + static const char kMid3Audio[] = "3_audio"; + static const char kMid4Video[] = "4_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Offer groups (kMid1Audio,kMid2Video) and (kMid3Audio,kMid4Video). + cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group1.AddContentName(kMid1Audio); + offer_bundle_group1.AddContentName(kMid2Video); + cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group2.AddContentName(kMid3Audio); + offer_bundle_group2.AddContentName(kMid4Video); + // Answer groups (kMid1Audio,kMid4Video) and (kMid3Audio,kMid2Video), i.e. the + // second group members have switched places. This should get rejected. + cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group1.AddContentName(kMid1Audio); + answer_bundle_group1.AddContentName(kMid4Video); + cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group2.AddContentName(kMid3Audio); + answer_bundle_group2.AddContentName(kMid2Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(offer_bundle_group1); + local_offer->AddGroup(offer_bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(answer_bundle_group1); + remote_answer->AddGroup(answer_bundle_group2); + + // Accept offer. + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + // Reject answer! + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); +} + +TEST_F(JsepTransportControllerTest, MultipleBundleGroupsInvalidSubsets) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Video[] = "2_video"; + static const char kMid3Audio[] = "3_audio"; + static const char kMid4Video[] = "4_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Offer groups (kMid1Audio,kMid2Video) and (kMid3Audio,kMid4Video). + cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group1.AddContentName(kMid1Audio); + offer_bundle_group1.AddContentName(kMid2Video); + cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group2.AddContentName(kMid3Audio); + offer_bundle_group2.AddContentName(kMid4Video); + // Answer groups (kMid1Audio) and (kMid2Video), i.e. the second group was + // moved from the first group. This should get rejected. + cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group1.AddContentName(kMid1Audio); + cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group2.AddContentName(kMid2Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(offer_bundle_group1); + local_offer->AddGroup(offer_bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(answer_bundle_group1); + remote_answer->AddGroup(answer_bundle_group2); + + // Accept offer. + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + // Reject answer! + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); +} + +TEST_F(JsepTransportControllerTest, MultipleBundleGroupsInvalidOverlap) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Video[] = "2_video"; + static const char kMid3Audio[] = "3_audio"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Offer groups (kMid1Audio,kMid3Audio) and (kMid2Video,kMid3Audio), i.e. + // kMid3Audio is in both groups - this is illegal. + cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group1.AddContentName(kMid1Audio); + offer_bundle_group1.AddContentName(kMid3Audio); + cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group2.AddContentName(kMid2Video); + offer_bundle_group2.AddContentName(kMid3Audio); + + auto offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + offer->AddGroup(offer_bundle_group1); + offer->AddGroup(offer_bundle_group2); + + // Reject offer, both if set as local or remote. + EXPECT_FALSE( + transport_controller_->SetLocalDescription(SdpType::kOffer, offer.get()) + .ok()); + EXPECT_FALSE( + transport_controller_->SetRemoteDescription(SdpType::kOffer, offer.get()) + .ok()); +} + +TEST_F(JsepTransportControllerTest, MultipleBundleGroupsUnbundleFirstMid) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Audio[] = "3_audio"; + static const char kMid4Video[] = "4_video"; + static const char kMid5Video[] = "5_video"; + static const char kMid6Video[] = "6_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Offer groups (kMid1Audio,kMid2Audio,kMid3Audio) and + // (kMid4Video,kMid5Video,kMid6Video). + cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group1.AddContentName(kMid1Audio); + offer_bundle_group1.AddContentName(kMid2Audio); + offer_bundle_group1.AddContentName(kMid3Audio); + cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group2.AddContentName(kMid4Video); + offer_bundle_group2.AddContentName(kMid5Video); + offer_bundle_group2.AddContentName(kMid6Video); + // Answer groups (kMid2Audio,kMid3Audio) and (kMid5Video,kMid6Video), i.e. + // we've moved the first MIDs out of the groups. + cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group1.AddContentName(kMid2Audio); + answer_bundle_group1.AddContentName(kMid3Audio); + cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group2.AddContentName(kMid5Video); + answer_bundle_group2.AddContentName(kMid6Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(offer_bundle_group1); + local_offer->AddGroup(offer_bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid6Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(answer_bundle_group1); + remote_answer->AddGroup(answer_bundle_group2); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio); + auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video); + auto mid5_transport = transport_controller_->GetRtpTransport(kMid5Video); + auto mid6_transport = transport_controller_->GetRtpTransport(kMid6Video); + EXPECT_NE(mid1_transport, mid2_transport); + EXPECT_EQ(mid2_transport, mid3_transport); + EXPECT_NE(mid4_transport, mid5_transport); + EXPECT_EQ(mid5_transport, mid6_transport); + EXPECT_NE(mid1_transport, mid4_transport); + EXPECT_NE(mid2_transport, mid5_transport); +} + +TEST_F(JsepTransportControllerTest, MultipleBundleGroupsChangeFirstMid) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Audio[] = "3_audio"; + static const char kMid4Video[] = "4_video"; + static const char kMid5Video[] = "5_video"; + static const char kMid6Video[] = "6_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Offer groups (kMid1Audio,kMid2Audio,kMid3Audio) and + // (kMid4Video,kMid5Video,kMid6Video). + cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group1.AddContentName(kMid1Audio); + offer_bundle_group1.AddContentName(kMid2Audio); + offer_bundle_group1.AddContentName(kMid3Audio); + cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group2.AddContentName(kMid4Video); + offer_bundle_group2.AddContentName(kMid5Video); + offer_bundle_group2.AddContentName(kMid6Video); + // Answer groups (kMid2Audio,kMid1Audio,kMid3Audio) and + // (kMid5Video,kMid6Video,kMid4Video), i.e. we've changed which MID is first + // but accept the whole group. + cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group1.AddContentName(kMid2Audio); + answer_bundle_group1.AddContentName(kMid1Audio); + answer_bundle_group1.AddContentName(kMid3Audio); + cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group2.AddContentName(kMid5Video); + answer_bundle_group2.AddContentName(kMid6Video); + answer_bundle_group2.AddContentName(kMid4Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(offer_bundle_group1); + local_offer->AddGroup(offer_bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid6Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(answer_bundle_group1); + remote_answer->AddGroup(answer_bundle_group2); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + + // The fact that we accept this answer is actually a bug. If we accept the + // first MID to be in the group, we should also accept that it is the tagged + // one. + // TODO(https://crbug.com/webrtc/12699): When this issue is fixed, change this + // to EXPECT_FALSE and remove the below expectations about transports. + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio); + auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video); + auto mid5_transport = transport_controller_->GetRtpTransport(kMid5Video); + auto mid6_transport = transport_controller_->GetRtpTransport(kMid6Video); + EXPECT_NE(mid1_transport, mid4_transport); + EXPECT_EQ(mid1_transport, mid2_transport); + EXPECT_EQ(mid2_transport, mid3_transport); + EXPECT_EQ(mid4_transport, mid5_transport); + EXPECT_EQ(mid5_transport, mid6_transport); +} + +TEST_F(JsepTransportControllerTest, + MultipleBundleGroupsSectionsAddedInSubsequentOffer) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Audio[] = "3_audio"; + static const char kMid4Video[] = "4_video"; + static const char kMid5Video[] = "5_video"; + static const char kMid6Video[] = "6_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Start by grouping (kMid1Audio,kMid2Audio) and (kMid4Video,kMid4f5Video). + cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE); + bundle_group1.AddContentName(kMid1Audio); + bundle_group1.AddContentName(kMid2Audio); + cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE); + bundle_group2.AddContentName(kMid4Video); + bundle_group2.AddContentName(kMid5Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(bundle_group1); + local_offer->AddGroup(bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(bundle_group1); + remote_answer->AddGroup(bundle_group2); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Add kMid3Audio and kMid6Video to the respective audio/video bundle groups. + cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + bundle_group1.AddContentName(kMid3Audio); + cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + bundle_group2.AddContentName(kMid6Video); + + auto subsequent_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(subsequent_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + subsequent_offer->AddGroup(bundle_group1); + subsequent_offer->AddGroup(bundle_group2); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get()) + .ok()); + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio); + auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video); + auto mid5_transport = transport_controller_->GetRtpTransport(kMid5Video); + auto mid6_transport = transport_controller_->GetRtpTransport(kMid6Video); + EXPECT_NE(mid1_transport, mid4_transport); + EXPECT_EQ(mid1_transport, mid2_transport); + EXPECT_EQ(mid2_transport, mid3_transport); + EXPECT_EQ(mid4_transport, mid5_transport); + EXPECT_EQ(mid5_transport, mid6_transport); +} + +TEST_F(JsepTransportControllerTest, + MultipleBundleGroupsCombinedInSubsequentOffer) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Video[] = "3_video"; + static const char kMid4Video[] = "4_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Start by grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video). + cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE); + bundle_group1.AddContentName(kMid1Audio); + bundle_group1.AddContentName(kMid2Audio); + cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE); + bundle_group2.AddContentName(kMid3Video); + bundle_group2.AddContentName(kMid4Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(bundle_group1); + local_offer->AddGroup(bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(bundle_group1); + remote_answer->AddGroup(bundle_group2); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Switch to grouping (kMid1Audio,kMid2Audio,kMid3Video,kMid4Video). + // This is a illegal without first removing m= sections from their groups. + cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE); + new_bundle_group.AddContentName(kMid1Audio); + new_bundle_group.AddContentName(kMid2Audio); + new_bundle_group.AddContentName(kMid3Video); + new_bundle_group.AddContentName(kMid4Video); + + auto subsequent_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + subsequent_offer->AddGroup(new_bundle_group); + EXPECT_FALSE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get()) + .ok()); +} + +TEST_F(JsepTransportControllerTest, + MultipleBundleGroupsSplitInSubsequentOffer) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Video[] = "3_video"; + static const char kMid4Video[] = "4_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Start by grouping (kMid1Audio,kMid2Audio,kMid3Video,kMid4Video). + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kMid1Audio); + bundle_group.AddContentName(kMid2Audio); + bundle_group.AddContentName(kMid3Video); + bundle_group.AddContentName(kMid4Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(bundle_group); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Switch to grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video). + // This is a illegal without first removing m= sections from their groups. + cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + new_bundle_group1.AddContentName(kMid1Audio); + new_bundle_group1.AddContentName(kMid2Audio); + cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + new_bundle_group2.AddContentName(kMid3Video); + new_bundle_group2.AddContentName(kMid4Video); + + auto subsequent_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + subsequent_offer->AddGroup(new_bundle_group1); + subsequent_offer->AddGroup(new_bundle_group2); + EXPECT_FALSE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get()) + .ok()); +} + +TEST_F(JsepTransportControllerTest, + MultipleBundleGroupsShuffledInSubsequentOffer) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Video[] = "3_video"; + static const char kMid4Video[] = "4_video"; + + CreateJsepTransportController(JsepTransportController::Config()); + // Start by grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video). + cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE); + bundle_group1.AddContentName(kMid1Audio); + bundle_group1.AddContentName(kMid2Audio); + cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE); + bundle_group2.AddContentName(kMid3Video); + bundle_group2.AddContentName(kMid4Video); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(bundle_group1); + local_offer->AddGroup(bundle_group2); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + remote_answer->AddGroup(bundle_group1); + remote_answer->AddGroup(bundle_group2); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Switch to grouping (kMid1Audio,kMid3Video) and (kMid2Audio,kMid3Video). + // This is a illegal without first removing m= sections from their groups. + cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + new_bundle_group1.AddContentName(kMid1Audio); + new_bundle_group1.AddContentName(kMid3Video); + cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + new_bundle_group2.AddContentName(kMid2Audio); + new_bundle_group2.AddContentName(kMid4Video); + + auto subsequent_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + subsequent_offer->AddGroup(new_bundle_group1); + subsequent_offer->AddGroup(new_bundle_group2); + EXPECT_FALSE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get()) + .ok()); +} + +// Tests that only a subset of all the m= sections are bundled. +TEST_F(JsepTransportControllerTest, BundleSubsetOfMediaSections) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Verifiy that only `kAudio1` and `kVideo1` are bundled. + auto transport1 = transport_controller_->GetRtpTransport(kAudioMid1); + auto transport2 = transport_controller_->GetRtpTransport(kAudioMid2); + auto transport3 = transport_controller_->GetRtpTransport(kVideoMid1); + EXPECT_NE(transport1, transport2); + EXPECT_EQ(transport1, transport3); + + auto it = changed_rtp_transport_by_mid_.find(kVideoMid1); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(transport1, it->second); + it = changed_rtp_transport_by_mid_.find(kAudioMid2); + EXPECT_TRUE(transport2 == it->second); +} + +// Tests that the initial offer/answer only have data section and audio/video +// sections are added in the subsequent offer. +TEST_F(JsepTransportControllerTest, BundleOnDataSectionInSubsequentOffer) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kDataMid1); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddDataSection(local_offer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddDataSection(remote_answer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + auto data_transport = transport_controller_->GetRtpTransport(kDataMid1); + + // Add audio/video sections in subsequent offer. + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + // Reset the bundle group and do another offer/answer exchange. + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + local_offer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + remote_answer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + auto audio_transport = transport_controller_->GetRtpTransport(kAudioMid1); + auto video_transport = transport_controller_->GetRtpTransport(kVideoMid1); + EXPECT_EQ(data_transport, audio_transport); + EXPECT_EQ(data_transport, video_transport); +} + +TEST_F(JsepTransportControllerTest, VideoDataRejectedInAnswer) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + bundle_group.AddContentName(kDataMid1); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddDataSection(local_offer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddDataSection(remote_answer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + // Reject video and data section. + remote_answer->contents()[1].rejected = true; + remote_answer->contents()[2].rejected = true; + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Verify the RtpTransport/DtlsTransport is destroyed correctly. + EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kVideoMid1)); + EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kDataMid1)); + // Verify the signals are fired correctly. + auto it = changed_rtp_transport_by_mid_.find(kVideoMid1); + ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end()); + EXPECT_EQ(nullptr, it->second); + auto it2 = changed_dtls_transport_by_mid_.find(kDataMid1); + ASSERT_TRUE(it2 != changed_dtls_transport_by_mid_.end()); + EXPECT_EQ(nullptr, it2->second); +} + +// Tests that changing the bundled MID in subsequent offer/answer exchange is +// not supported. +// TODO(bugs.webrtc.org/6704): Change this test to expect success once issue is +// fixed +TEST_F(JsepTransportControllerTest, ChangeBundledMidNotSupported) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + EXPECT_EQ(transport_controller_->GetRtpTransport(kAudioMid1), + transport_controller_->GetRtpTransport(kVideoMid1)); + + // Reorder the bundle group. + EXPECT_TRUE(bundle_group.RemoveContentName(kAudioMid1)); + bundle_group.AddContentName(kAudioMid1); + // The answerer uses the new bundle group and now the bundle mid is changed to + // `kVideo1`. + remote_answer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + remote_answer->AddGroup(bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); +} +// Test that rejecting only the first m= section of a BUNDLE group is treated as +// an error, but rejecting all of them works as expected. +TEST_F(JsepTransportControllerTest, RejectFirstContentInBundleGroup) { + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + bundle_group.AddContentName(kVideoMid1); + bundle_group.AddContentName(kDataMid1); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddDataSection(local_offer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + AddDataSection(remote_answer.get(), kDataMid1, + cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + // Reject audio content in answer. + remote_answer->contents()[0].rejected = true; + + local_offer->AddGroup(bundle_group); + remote_answer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Reject all the contents. + remote_answer->contents()[1].rejected = true; + remote_answer->contents()[2].rejected = true; + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kAudioMid1)); + EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kVideoMid1)); + EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kDataMid1)); +} + +// Tests that applying non-RTCP-mux offer would fail when kRtcpMuxPolicyRequire +// is used. +TEST_F(JsepTransportControllerTest, ApplyNonRtcpMuxOfferWhenMuxingRequired) { + JsepTransportController::Config config; + config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyRequire; + CreateJsepTransportController(config); + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + + local_offer->contents()[0].media_description()->set_rtcp_mux(false); + // Applying a non-RTCP-mux offer is expected to fail. + EXPECT_FALSE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); +} + +// Tests that applying non-RTCP-mux answer would fail when kRtcpMuxPolicyRequire +// is used. +TEST_F(JsepTransportControllerTest, ApplyNonRtcpMuxAnswerWhenMuxingRequired) { + JsepTransportController::Config config; + config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyRequire; + CreateJsepTransportController(config); + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + + auto remote_answer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE, + nullptr); + // Applying a non-RTCP-mux answer is expected to fail. + remote_answer->contents()[0].media_description()->set_rtcp_mux(false); + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); +} + +// This tests that the BUNDLE group in answer should be a subset of the offered +// group. +TEST_F(JsepTransportControllerTest, + AddContentToBundleGroupInAnswerNotSupported) { + CreateJsepTransportController(JsepTransportController::Config()); + auto local_offer = CreateSessionDescriptionWithoutBundle(); + auto remote_answer = CreateSessionDescriptionWithoutBundle(); + + cricket::ContentGroup offer_bundle_group(cricket::GROUP_TYPE_BUNDLE); + offer_bundle_group.AddContentName(kAudioMid1); + local_offer->AddGroup(offer_bundle_group); + + cricket::ContentGroup answer_bundle_group(cricket::GROUP_TYPE_BUNDLE); + answer_bundle_group.AddContentName(kAudioMid1); + answer_bundle_group.AddContentName(kVideoMid1); + remote_answer->AddGroup(answer_bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); +} + +// This tests that the BUNDLE group with non-existing MID should be rejectd. +TEST_F(JsepTransportControllerTest, RejectBundleGroupWithNonExistingMid) { + CreateJsepTransportController(JsepTransportController::Config()); + auto local_offer = CreateSessionDescriptionWithoutBundle(); + auto remote_answer = CreateSessionDescriptionWithoutBundle(); + + cricket::ContentGroup invalid_bundle_group(cricket::GROUP_TYPE_BUNDLE); + // The BUNDLE group is invalid because there is no data section in the + // description. + invalid_bundle_group.AddContentName(kDataMid1); + local_offer->AddGroup(invalid_bundle_group); + remote_answer->AddGroup(invalid_bundle_group); + + EXPECT_FALSE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); +} + +// This tests that an answer shouldn't be able to remove an m= section from an +// established group without rejecting it. +TEST_F(JsepTransportControllerTest, RemoveContentFromBundleGroup) { + CreateJsepTransportController(JsepTransportController::Config()); + + auto local_offer = CreateSessionDescriptionWithBundleGroup(); + auto remote_answer = CreateSessionDescriptionWithBundleGroup(); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Do an re-offer/answer. + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + auto new_answer = CreateSessionDescriptionWithoutBundle(); + cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE); + // The answer removes video from the BUNDLE group without rejecting it is + // invalid. + new_bundle_group.AddContentName(kAudioMid1); + new_answer->AddGroup(new_bundle_group); + + // Applying invalid answer is expected to fail. + EXPECT_FALSE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, new_answer.get()) + .ok()); + + // Rejected the video content. + auto video_content = new_answer->GetContentByName(kVideoMid1); + ASSERT_TRUE(video_content); + video_content->rejected = true; + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, new_answer.get()) + .ok()); +} + +// Test that the JsepTransportController can process a new local and remote +// description that changes the tagged BUNDLE group with the max-bundle policy +// specified. +// This is a regression test for bugs.webrtc.org/9954 +TEST_F(JsepTransportControllerTest, ChangeTaggedMediaSectionMaxBundle) { + CreateJsepTransportController(JsepTransportController::Config()); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kAudioMid1); + local_offer->AddGroup(bundle_group); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + + std::unique_ptr<cricket::SessionDescription> remote_answer( + local_offer->Clone()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + std::unique_ptr<cricket::SessionDescription> local_reoffer( + local_offer->Clone()); + local_reoffer->contents()[0].rejected = true; + AddVideoSection(local_reoffer.get(), kVideoMid1, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_reoffer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE); + cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE); + new_bundle_group.AddContentName(kVideoMid1); + local_reoffer->AddGroup(new_bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_reoffer.get()) + .ok()); + std::unique_ptr<cricket::SessionDescription> remote_reanswer( + local_reoffer->Clone()); + EXPECT_TRUE( + transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_reanswer.get()) + .ok()); +} + +TEST_F(JsepTransportControllerTest, RollbackRestoresRejectedTransport) { + static const char kMid1Audio[] = "1_audio"; + + // Perform initial offer/answer. + CreateJsepTransportController(JsepTransportController::Config()); + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + std::unique_ptr<cricket::SessionDescription> remote_answer( + local_offer->Clone()); + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + + // Apply a reoffer which rejects the m= section, causing the transport to be + // set to null. + auto local_reoffer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_reoffer->contents()[0].rejected = true; + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_reoffer.get()) + .ok()); + auto old_mid1_transport = mid1_transport; + mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + EXPECT_EQ(nullptr, mid1_transport); + + // Rolling back shouldn't just create a new transport for MID 1, it should + // restore the old transport. + EXPECT_TRUE(transport_controller_->RollbackTransports().ok()); + mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + EXPECT_EQ(old_mid1_transport, mid1_transport); +} + +// If an offer with a modified BUNDLE group causes a MID->transport mapping to +// change, rollback should restore the previous mapping. +TEST_F(JsepTransportControllerTest, RollbackRestoresPreviousTransportMapping) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Audio[] = "3_audio"; + + // Perform an initial offer/answer to establish a (kMid1Audio,kMid2Audio) + // group. + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE); + bundle_group.AddContentName(kMid1Audio); + bundle_group.AddContentName(kMid2Audio); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(bundle_group); + + std::unique_ptr<cricket::SessionDescription> remote_answer( + local_offer->Clone()); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio); + auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + EXPECT_EQ(mid1_transport, mid2_transport); + EXPECT_NE(mid1_transport, mid3_transport); + + // Apply a reoffer adding kMid3Audio to the group; transport mapping should + // change, even without an answer, since this is an existing group. + bundle_group.AddContentName(kMid3Audio); + auto local_reoffer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_reoffer.get(), kMid2Audio, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddAudioSection(local_reoffer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_reoffer->AddGroup(bundle_group); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_reoffer.get()) + .ok()); + + // Store the old transport pointer and verify that the offer actually changed + // transports. + auto old_mid3_transport = mid3_transport; + mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio); + mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + EXPECT_EQ(mid1_transport, mid2_transport); + EXPECT_EQ(mid1_transport, mid3_transport); + + // Rolling back shouldn't just create a new transport for MID 3, it should + // restore the old transport. + EXPECT_TRUE(transport_controller_->RollbackTransports().ok()); + mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + EXPECT_EQ(old_mid3_transport, mid3_transport); +} + +// Test that if an offer adds a MID to a specific BUNDLE group and is then +// rolled back, it can be added to a different BUNDLE group in a new offer. +// This is effectively testing that rollback resets the BundleManager state. +TEST_F(JsepTransportControllerTest, RollbackAndAddToDifferentBundleGroup) { + static const char kMid1Audio[] = "1_audio"; + static const char kMid2Audio[] = "2_audio"; + static const char kMid3Audio[] = "3_audio"; + + // Perform an initial offer/answer to establish two bundle groups, each with + // one MID. + CreateJsepTransportController(JsepTransportController::Config()); + cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE); + bundle_group1.AddContentName(kMid1Audio); + cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE); + bundle_group2.AddContentName(kMid2Audio); + + auto local_offer = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + local_offer->AddGroup(bundle_group1); + local_offer->AddGroup(bundle_group2); + + std::unique_ptr<cricket::SessionDescription> remote_answer( + local_offer->Clone()); + + EXPECT_TRUE(transport_controller_ + ->SetLocalDescription(SdpType::kOffer, local_offer.get()) + .ok()); + EXPECT_TRUE(transport_controller_ + ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get()) + .ok()); + + // Apply an offer that adds kMid3Audio to the first BUNDLE group., + cricket::ContentGroup modified_bundle_group1(cricket::GROUP_TYPE_BUNDLE); + modified_bundle_group1.AddContentName(kMid1Audio); + modified_bundle_group1.AddContentName(kMid3Audio); + auto subsequent_offer_1 = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(subsequent_offer_1.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer_1.get(), kMid2Audio, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer_1.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + subsequent_offer_1->AddGroup(modified_bundle_group1); + subsequent_offer_1->AddGroup(bundle_group2); + + EXPECT_TRUE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, subsequent_offer_1.get()) + .ok()); + + auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio); + auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + EXPECT_NE(mid1_transport, mid2_transport); + EXPECT_EQ(mid1_transport, mid3_transport); + + // Rollback and expect the transport to be reset. + EXPECT_TRUE(transport_controller_->RollbackTransports().ok()); + EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kMid3Audio)); + + // Apply an offer that adds kMid3Audio to the second BUNDLE group., + cricket::ContentGroup modified_bundle_group2(cricket::GROUP_TYPE_BUNDLE); + modified_bundle_group2.AddContentName(kMid2Audio); + modified_bundle_group2.AddContentName(kMid3Audio); + auto subsequent_offer_2 = std::make_unique<cricket::SessionDescription>(); + AddAudioSection(subsequent_offer_2.get(), kMid1Audio, kIceUfrag1, kIcePwd1, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer_2.get(), kMid2Audio, kIceUfrag2, kIcePwd2, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + AddVideoSection(subsequent_offer_2.get(), kMid3Audio, kIceUfrag3, kIcePwd3, + cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS, + nullptr); + subsequent_offer_2->AddGroup(bundle_group1); + subsequent_offer_2->AddGroup(modified_bundle_group2); + + EXPECT_TRUE( + transport_controller_ + ->SetLocalDescription(SdpType::kOffer, subsequent_offer_2.get()) + .ok()); + + mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio); + mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio); + mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio); + EXPECT_NE(mid1_transport, mid2_transport); + EXPECT_EQ(mid2_transport, mid3_transport); +} + +} // namespace webrtc |