Index: webrtc/p2p/quic/quictransportchannel_unittest.cc |
diff --git a/webrtc/p2p/quic/quictransportchannel_unittest.cc b/webrtc/p2p/quic/quictransportchannel_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..bb532a3f9a28e275c90cd26ddc831f7b67e5f1e3 |
--- /dev/null |
+++ b/webrtc/p2p/quic/quictransportchannel_unittest.cc |
@@ -0,0 +1,480 @@ |
+/* |
+ * Copyright 2016 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 "webrtc/p2p/quic/quictransportchannel.h" |
+ |
+#include <set> |
+#include <string> |
+#include <vector> |
+ |
+#include "webrtc/base/common.h" |
+#include "webrtc/base/gunit.h" |
+#include "webrtc/base/scoped_ptr.h" |
+#include "webrtc/base/sslidentity.h" |
+#include "webrtc/p2p/base/faketransportcontroller.h" |
+ |
+using cricket::ConnectionRole; |
+using cricket::IceRole; |
+using cricket::QuicTransportChannel; |
+using cricket::TransportChannel; |
+using cricket::TransportDescription; |
+ |
+// Timeout in milliseconds for asynchronous operations in unit tests. |
+const int kTimeoutMs = 1000; |
honghaiz3
2016/03/02 22:47:45
Mark all const and helper methods as static or put
mikescarlett
2016/03/03 02:19:20
Done.
|
+ |
+// Export keying material parameters. |
+const char kExporterLabel[] = "label"; |
+const uint8_t kExporterContext[] = "context"; |
+const size_t kExporterContextLength = sizeof(kExporterContext); |
+const size_t kOutputKeyLength = 20; |
+ |
+// Packet size for SRTP. |
+const size_t kPacketSize = 100; |
+ |
+// Indicates channel has no write error. |
+const int kNoWriteError = 0; |
+ |
+// ICE parameters. |
+const char kIceUfrag[] = "TESTICEUFRAG0001"; |
+const char kIcePwd[] = "TESTICEPWD00000000000001"; |
+ |
+// QUIC packet parameters. |
+const net::IPAddressNumber kIpAddress(net::kIPv4AddressSize, 0); |
+const net::IPEndPoint kIpEndpoint(kIpAddress, 0); |
+ |
+// Detects incoming RTP packets. |
+bool IsRtpLeadByte(uint8_t b) { |
+ return (b & 0xC0) == 0x80; |
+} |
+// Detects incoming QUIC packets. |
+bool IsQuicLeadByte(uint8_t b) { |
+ return (b & 0x80) == 0; |
+} |
+ |
+// Maps SSL role to ICE connection role. The peer with a client role is assumed |
+// to be the one who initiates the connection. |
+ConnectionRole SslRoleToConnectionRole(rtc::SSLRole ssl_role) { |
+ return (ssl_role == rtc::SSL_CLIENT) ? cricket::CONNECTIONROLE_ACTIVE |
+ : cricket::CONNECTIONROLE_PASSIVE; |
+} |
+ |
+// Allows cricket::FakeTransportChannel to simulate write blocked |
+// and write error states. |
+// TODO(mikescarlett): Add this functionality to cricket::FakeTransportChannel. |
+class FailableTransportChannel : public cricket::FakeTransportChannel { |
+ public: |
+ FailableTransportChannel(const std::string& name, int component) |
+ : cricket::FakeTransportChannel(name, component), error_(kNoWriteError) {} |
+ int GetError() override { return error_; } |
+ void SetError(int error) { error_ = error; } |
+ int SendPacket(const char* data, |
+ size_t len, |
+ const rtc::PacketOptions& options, |
+ int flags) override { |
+ if (error_ == kNoWriteError) { |
+ return cricket::FakeTransportChannel::SendPacket(data, len, options, |
+ flags); |
+ } |
+ return -1; |
+ } |
+ |
+ private: |
+ int error_; |
+}; |
+ |
+// Peer who establishes a handshake using a QuicTransportChannel, which wraps |
+// a FailableTransportChannel to simulate network connectivity and ICE |
+// negotiation. |
+class QuicTestPeer : public sigslot::has_slots<> { |
+ public: |
+ explicit QuicTestPeer(const std::string& name) |
+ : name_(name), |
+ bytes_sent_(0), |
+ fake_channel_(name_, 0), |
+ quic_channel_(&fake_channel_) { |
+ quic_channel_.SignalReadPacket.connect( |
+ this, &QuicTestPeer::OnTransportChannelReadPacket); |
+ fake_channel_.SetAsync(true); |
+ SetCryptoParameters(); |
+ } |
+ |
+ // Set this peer's local certificate and fingerprint, asserting that |
+ // these are valid. |
+ void SetCryptoParameters() { |
+ rtc::scoped_refptr<rtc::RTCCertificate> local_cert = |
+ rtc::RTCCertificate::Create(rtc::scoped_ptr<rtc::SSLIdentity>( |
+ rtc::SSLIdentity::Generate(name_, rtc::KT_DEFAULT))); |
+ ASSERT_NE(nullptr, local_cert); |
+ quic_channel_.SetLocalCertificate(local_cert); |
+ local_fingerprint_.reset(CreateFingerprint(local_cert.get())); |
+ ASSERT_NE(nullptr, local_fingerprint_); |
+ } |
+ |
+ // Connects |fake_channel_| to that of the other peer. |
+ void Connect(QuicTestPeer* other_peer) { |
+ fake_channel_.Connect(); |
+ other_peer->fake_channel_.Connect(); |
+ fake_channel_.SetDestination(&other_peer->fake_channel_); |
+ } |
+ |
+ // Disconnects |fake_channel_|. |
+ void Disconnect() { fake_channel_.SetDestination(nullptr); } |
+ |
+ // Generates ICE credentials and passes them to |quic_channel_|. |
+ void SetIceParameters(IceRole local_ice_role, |
+ ConnectionRole local_connection_role, |
+ ConnectionRole remote_connection_role, |
+ rtc::SSLFingerprint* remote_fingerprint) { |
+ quic_channel_.SetIceRole(local_ice_role); |
+ quic_channel_.SetIceTiebreaker( |
+ (local_ice_role == cricket::ICEROLE_CONTROLLING) ? 1 : 2); |
+ |
+ TransportDescription local_desc( |
+ std::vector<std::string>(), kIceUfrag, kIcePwd, cricket::ICEMODE_FULL, |
+ local_connection_role, local_fingerprint_.get()); |
+ TransportDescription remote_desc( |
+ std::vector<std::string>(), kIceUfrag, kIcePwd, cricket::ICEMODE_FULL, |
+ remote_connection_role, remote_fingerprint); |
+ |
+ quic_channel_.SetIceCredentials(local_desc.ice_ufrag, local_desc.ice_pwd); |
+ quic_channel_.SetRemoteIceCredentials(remote_desc.ice_ufrag, |
+ remote_desc.ice_pwd); |
+ } |
+ |
+ // Creates fingerprint from certificate. |
+ rtc::SSLFingerprint* CreateFingerprint(rtc::RTCCertificate* cert) { |
+ std::string digest_algorithm; |
+ bool get_digest_algorithm = |
+ cert->ssl_certificate().GetSignatureDigestAlgorithm(&digest_algorithm); |
+ if (!get_digest_algorithm || digest_algorithm.empty()) { |
+ return nullptr; |
+ } |
+ scoped_ptr<rtc::SSLFingerprint> fingerprint( |
+ rtc::SSLFingerprint::Create(digest_algorithm, cert->identity())); |
+ if (digest_algorithm != rtc::DIGEST_SHA_256) { |
+ return nullptr; |
+ } |
+ return fingerprint.release(); |
+ } |
+ |
+ // Send SRTP packet to the other peer via |quic_channel_|. |
+ int SendSrtpPacket() { |
+ char packet[kPacketSize]; |
+ packet[0] = 0x80; // Make the packet header look like RTP. |
+ int rv = quic_channel_.SendPacket( |
+ &packet[0], kPacketSize, rtc::PacketOptions(), cricket::PF_SRTP_BYPASS); |
+ bytes_sent_ += rv; |
+ return rv; |
+ } |
+ |
+ // Sends a non-SRTP packet with the PF_SRTP_BYPASS flag via |quic_channel_|. |
+ int SendInvalidSrtpPacket() { |
+ char packet[kPacketSize]; |
+ // Fill the packet with 0 to form an invalid SRTP packet. |
+ memset(packet, 0, kPacketSize); |
+ return quic_channel_.SendPacket( |
+ &packet[0], kPacketSize, rtc::PacketOptions(), cricket::PF_SRTP_BYPASS); |
+ } |
+ |
+ // Sends an RTP packet to the other peer via |quic_channel_|, without the SRTP |
+ // bypass flag. |
+ int SendRtpPacket() { |
+ char packet[kPacketSize]; |
+ packet[0] = 0x80; // Make the packet header look like RTP. |
+ return quic_channel_.SendPacket(&packet[0], kPacketSize, |
+ rtc::PacketOptions(), 0); |
+ } |
+ |
+ void ClearBytesSent() { bytes_sent_ = 0; } |
+ |
+ void ClearBytesReceived() { bytes_received_ = 0; } |
+ |
+ void SetWriteError(int error) { fake_channel_.SetError(error); } |
+ |
+ size_t bytes_received() const { return bytes_received_; } |
+ |
+ size_t bytes_sent() const { return bytes_sent_; } |
+ |
+ FailableTransportChannel* ice_channel() { return &fake_channel_; } |
+ |
+ QuicTransportChannel* quic_channel() { return &quic_channel_; } |
+ |
+ rtc::scoped_ptr<rtc::SSLFingerprint>& local_fingerprint() { |
+ return local_fingerprint_; |
+ } |
+ |
+ private: |
+ // QUIC channel callback. |
+ void OnTransportChannelReadPacket(TransportChannel* channel, |
+ const char* data, |
+ size_t size, |
+ const rtc::PacketTime& packet_time, |
+ int flags) { |
+ bytes_received_ += size; |
+ // Only SRTP packets should have the bypass flag set. |
+ int expected_flags = IsRtpLeadByte(data[0]) ? cricket::PF_SRTP_BYPASS : 0; |
+ ASSERT_EQ(expected_flags, flags); |
+ } |
+ |
+ std::string name_; // Channel name. |
+ size_t bytes_sent_; // Bytes sent by QUIC channel. |
+ size_t bytes_received_; // Bytes received by fake channel. |
+ FailableTransportChannel fake_channel_; // Simulates an ICE channel. |
pthatcher1
2016/03/02 19:48:13
Might as well make it ice_channel_ here as well.
mikescarlett
2016/03/03 02:19:20
Done.
|
+ QuicTransportChannel quic_channel_; // QUIC channel to test. |
+ rtc::scoped_ptr<rtc::SSLFingerprint> local_fingerprint_; |
+}; |
+ |
+class QuicTransportChannelTest : public testing::Test { |
+ public: |
+ QuicTransportChannelTest() : peer1_("P1"), peer2_("P2") {} |
+ |
+ // Performs negotiation before QUIC handshake, then connects the fake |
+ // transport channels of each peer. As a side effect, the QUIC channels |
+ // start sending handshake messages. |peer1_| has a client role and |peer2_| |
+ // has server role in the QUIC handshake. |
+ void Connect() { |
+ NegotiateBeforeQuic(rtc::SSL_CLIENT, rtc::SSL_SERVER); |
+ peer1_.Connect(&peer2_); |
+ } |
+ |
+ // Disconnects the fake transport channels. |
+ void Disconnect() { |
+ peer1_.Disconnect(); |
+ peer2_.Disconnect(); |
+ } |
+ |
+ // Sets up ICE parameters and exchanges fingerprints before QUIC handshake. |
+ void NegotiateBeforeQuic(rtc::SSLRole peer1_ssl_role, |
+ rtc::SSLRole peer2_ssl_role) { |
pthatcher1
2016/03/02 19:48:13
Can you rename this SetIceAndCryptoParameters()?
mikescarlett
2016/03/03 02:19:20
Done.
|
+ peer1_.quic_channel()->SetSslRole(peer1_ssl_role); |
+ peer2_.quic_channel()->SetSslRole(peer2_ssl_role); |
+ |
+ rtc::scoped_ptr<rtc::SSLFingerprint>& peer1_fingerprint = |
+ peer1_.local_fingerprint(); |
+ rtc::scoped_ptr<rtc::SSLFingerprint>& peer2_fingerprint = |
+ peer2_.local_fingerprint(); |
+ |
+ peer1_.quic_channel()->SetRemoteFingerprint( |
+ peer2_fingerprint->algorithm, |
+ reinterpret_cast<const uint8_t*>(peer2_fingerprint->digest.data()), |
+ peer2_fingerprint->digest.size()); |
+ peer2_.quic_channel()->SetRemoteFingerprint( |
+ peer1_fingerprint->algorithm, |
+ reinterpret_cast<const uint8_t*>(peer1_fingerprint->digest.data()), |
+ peer1_fingerprint->digest.size()); |
+ |
+ ConnectionRole peer1_connection_role = |
+ SslRoleToConnectionRole(peer1_ssl_role); |
+ ConnectionRole peer2_connection_role = |
+ SslRoleToConnectionRole(peer2_ssl_role); |
+ |
+ peer1_.SetIceParameters(cricket::ICEROLE_CONTROLLED, peer1_connection_role, |
+ peer2_connection_role, peer2_fingerprint.get()); |
+ peer2_.SetIceParameters(cricket::ICEROLE_CONTROLLING, |
+ peer2_connection_role, peer1_connection_role, |
+ peer1_fingerprint.get()); |
+ } |
+ |
+ // Checks if QUIC handshake is done. |
+ bool quic_connected() { |
+ return peer1_.quic_channel()->quic_state() == |
+ cricket::QUIC_TRANSPORT_CONNECTED && |
+ peer2_.quic_channel()->quic_state() == |
+ cricket::QUIC_TRANSPORT_CONNECTED; |
+ } |
+ |
+ // Checks if QUIC channels are writable. |
+ bool quic_writable() { |
+ return peer1_.quic_channel()->writable() && |
+ peer2_.quic_channel()->writable(); |
+ } |
+ |
+ protected: |
+ // QUIC peer with a client role, who initiates the QUIC handshake. |
+ QuicTestPeer peer1_; |
+ // QUIC peer with a server role, who responds to the client peer. |
+ QuicTestPeer peer2_; |
+}; |
+ |
+// Test that the QUIC channel passes ICE parameters to the underlying ICE |
+// channel. |
+TEST_F(QuicTransportChannelTest, ChannelSetupIce) { |
+ NegotiateBeforeQuic(rtc::SSL_CLIENT, rtc::SSL_SERVER); |
+ FailableTransportChannel* channel1 = peer1_.ice_channel(); |
+ FailableTransportChannel* channel2 = peer2_.ice_channel(); |
+ EXPECT_EQ(cricket::ICEROLE_CONTROLLED, channel1->GetIceRole()); |
+ EXPECT_EQ(2u, channel1->IceTiebreaker()); |
+ EXPECT_EQ(kIceUfrag, channel1->ice_ufrag()); |
+ EXPECT_EQ(kIcePwd, channel1->ice_pwd()); |
+ EXPECT_EQ(cricket::ICEROLE_CONTROLLING, channel2->GetIceRole()); |
+ EXPECT_EQ(1u, channel2->IceTiebreaker()); |
+} |
+ |
+// Test that export keying material generates identical keys for both peers |
+// after QUIC handshake. |
+TEST_F(QuicTransportChannelTest, ExportKeyingMaterial) { |
+ Connect(); |
+ ASSERT_TRUE_WAIT(quic_connected(), kTimeoutMs); |
+ uint8_t key1[kOutputKeyLength]; |
+ uint8_t key2[kOutputKeyLength]; |
+ |
+ bool from_success = peer1_.quic_channel()->ExportKeyingMaterial( |
+ kExporterLabel, kExporterContext, kExporterContextLength, true, key1, |
+ kOutputKeyLength); |
+ ASSERT_TRUE(from_success); |
+ bool to_success = peer2_.quic_channel()->ExportKeyingMaterial( |
+ kExporterLabel, kExporterContext, kExporterContextLength, true, key2, |
+ kOutputKeyLength); |
+ ASSERT_TRUE(to_success); |
+ |
+ EXPECT_EQ(0, memcmp(key1, key2, sizeof(key1))); |
+} |
+ |
+// Test that QUIC channel is not writable before the QUIC handshake. |
+TEST_F(QuicTransportChannelTest, NotWritableBeforeHandshake) { |
+ Connect(); |
+ EXPECT_FALSE(quic_writable()); |
+ Disconnect(); |
+ EXPECT_FALSE(quic_writable()); |
+ Connect(); |
+ EXPECT_FALSE(quic_writable()); |
+} |
+ |
+// Test that once handshake begins, QUIC is not writable until its completion. |
+TEST_F(QuicTransportChannelTest, QuicHandshake) { |
+ Connect(); |
+ EXPECT_FALSE(quic_writable()); |
+ ASSERT_TRUE_WAIT(quic_connected(), kTimeoutMs); |
+ EXPECT_TRUE(quic_writable()); |
+} |
+ |
+// Non-SRTP data should not be sent using SendPacket(), regardless of QUIC |
+// channel state. |
+TEST_F(QuicTransportChannelTest, TransferNonSrtp) { |
+ // Send data before ICE channel is connected. |
+ peer1_.ClearBytesSent(); |
+ peer2_.ClearBytesReceived(); |
+ ASSERT_EQ(-1, peer1_.SendRtpPacket()); |
+ EXPECT_EQ(0u, peer1_.bytes_sent()); |
+ // Send data after ICE channel is connected, before QUIC handshake. |
+ Connect(); |
+ peer1_.ClearBytesSent(); |
+ peer2_.ClearBytesReceived(); |
+ ASSERT_EQ(-1, peer1_.SendRtpPacket()); |
+ EXPECT_EQ(0u, peer1_.bytes_sent()); |
+ // Send data after QUIC handshake. |
+ ASSERT_TRUE_WAIT(quic_connected(), kTimeoutMs); |
+ peer1_.ClearBytesSent(); |
+ peer2_.ClearBytesReceived(); |
+ ASSERT_EQ(-1, peer1_.SendRtpPacket()); |
+ EXPECT_EQ(0u, peer1_.bytes_sent()); |
+} |
+ |
+// SRTP data should always be sent, regardless of QUIC channel state, when the |
+// ICE channel is connected. |
+TEST_F(QuicTransportChannelTest, TransferSrtp) { |
+ // Send data after ICE channel is connected, before QUIC handshake. |
+ Connect(); |
+ peer1_.ClearBytesSent(); |
+ peer2_.ClearBytesReceived(); |
+ ASSERT_EQ(kPacketSize, static_cast<size_t>(peer1_.SendSrtpPacket())); |
+ EXPECT_EQ_WAIT(kPacketSize, peer2_.bytes_received(), kTimeoutMs); |
+ EXPECT_EQ(kPacketSize, peer1_.bytes_sent()); |
+ ASSERT_TRUE_WAIT(quic_connected(), kTimeoutMs); |
+ // Send data after QUIC handshake. |
+ peer1_.ClearBytesSent(); |
+ peer2_.ClearBytesReceived(); |
+ ASSERT_EQ(kPacketSize, static_cast<size_t>(peer1_.SendSrtpPacket())); |
+ EXPECT_EQ_WAIT(kPacketSize, peer2_.bytes_received(), kTimeoutMs); |
+ EXPECT_EQ(kPacketSize, peer1_.bytes_sent()); |
+} |
+ |
+// Test that invalid SRTP (non-SRTP data with |
+// PF_SRTP_BYPASS flag) fails to send with return value -1. |
+TEST_F(QuicTransportChannelTest, TransferInvalidSrtp) { |
+ peer1_.ClearBytesSent(); |
+ peer2_.ClearBytesReceived(); |
+ EXPECT_EQ(-1, peer1_.SendInvalidSrtpPacket()); |
+ EXPECT_EQ(0u, peer2_.bytes_received()); |
+ Connect(); |
+ peer1_.ClearBytesSent(); |
+ peer2_.ClearBytesReceived(); |
+ EXPECT_EQ(-1, peer1_.SendInvalidSrtpPacket()); |
+ EXPECT_EQ(0u, peer2_.bytes_received()); |
+} |
+ |
+// Test that QuicTransportChannel::WritePacket blocks when the ICE |
+// channel is not writable, and otherwise succeeds. |
+TEST_F(QuicTransportChannelTest, QuicWritePacket) { |
+ peer1_.ice_channel()->Connect(); |
+ peer2_.ice_channel()->Connect(); |
+ peer1_.ice_channel()->SetDestination(peer2_.ice_channel()); |
+ std::string packet = "FAKEQUICPACKET"; |
+ |
+ // QUIC should be write blocked when the ICE channel is not writable. |
+ peer1_.ice_channel()->SetWritable(false); |
+ EXPECT_TRUE(peer1_.quic_channel()->IsWriteBlocked()); |
+ net::WriteResult write_blocked_result = peer1_.quic_channel()->WritePacket( |
+ packet.data(), packet.size(), kIpAddress, kIpEndpoint); |
+ EXPECT_EQ(net::WRITE_STATUS_BLOCKED, write_blocked_result.status); |
+ EXPECT_EQ(EWOULDBLOCK, write_blocked_result.error_code); |
+ |
+ // QUIC should ignore errors when the ICE channel is writable. |
+ peer1_.ice_channel()->SetWritable(true); |
+ EXPECT_FALSE(peer1_.quic_channel()->IsWriteBlocked()); |
+ peer1_.SetWriteError(EWOULDBLOCK); |
+ net::WriteResult ignore_error_result = peer1_.quic_channel()->WritePacket( |
+ packet.data(), packet.size(), kIpAddress, kIpEndpoint); |
+ EXPECT_EQ(net::WRITE_STATUS_OK, ignore_error_result.status); |
+ EXPECT_EQ(0, ignore_error_result.bytes_written); |
+ |
+ peer1_.SetWriteError(kNoWriteError); |
+ net::WriteResult no_error_result = peer1_.quic_channel()->WritePacket( |
+ packet.data(), packet.size(), kIpAddress, kIpEndpoint); |
+ EXPECT_EQ(net::WRITE_STATUS_OK, no_error_result.status); |
+ EXPECT_EQ(static_cast<int>(packet.size()), no_error_result.bytes_written); |
+} |
+ |
+// Test that SSL roles can be reversed before QUIC handshake. |
+TEST_F(QuicTransportChannelTest, QuicRoleReversalBeforeQuic) { |
+ EXPECT_TRUE(peer1_.quic_channel()->SetSslRole(rtc::SSL_SERVER)); |
+ EXPECT_TRUE(peer1_.quic_channel()->SetSslRole(rtc::SSL_CLIENT)); |
+ EXPECT_TRUE(peer1_.quic_channel()->SetSslRole(rtc::SSL_SERVER)); |
+} |
+ |
+// Test that SSL roles cannot be reversed after QUIC handshake. SetSslRole |
+// returns true if the current SSL role equals the proposed SSL role. |
+TEST_F(QuicTransportChannelTest, QuicRoleReversalAfterQuic) { |
+ Connect(); |
+ ASSERT_TRUE_WAIT(quic_connected(), kTimeoutMs); |
+ EXPECT_FALSE(peer1_.quic_channel()->SetSslRole(rtc::SSL_SERVER)); |
+ EXPECT_TRUE(peer1_.quic_channel()->SetSslRole(rtc::SSL_CLIENT)); |
+ EXPECT_FALSE(peer2_.quic_channel()->SetSslRole(rtc::SSL_CLIENT)); |
+ EXPECT_TRUE(peer2_.quic_channel()->SetSslRole(rtc::SSL_SERVER)); |
+} |
+ |
+// Set SSL role, then check that GetSslRole returns the same value. |
+TEST_F(QuicTransportChannelTest, SetGetSslRole) { |
+ ASSERT_TRUE(peer1_.quic_channel()->SetSslRole(rtc::SSL_SERVER)); |
+ rtc::scoped_ptr<rtc::SSLRole> role(new rtc::SSLRole()); |
+ ASSERT_TRUE(peer1_.quic_channel()->GetSslRole(role.get())); |
+ EXPECT_EQ(rtc::SSL_SERVER, *role); |
+} |
+ |
+// Test that after QUIC handshake is complete, QUIC handshake remains confirmed |
+// even if the ICE channel reconnects. |
+TEST_F(QuicTransportChannelTest, HandshakeConfirmedAfterReconnect) { |
+ Connect(); |
+ ASSERT_TRUE_WAIT(quic_connected(), kTimeoutMs); |
+ Disconnect(); |
+ EXPECT_TRUE(quic_connected()); |
+ Connect(); |
+ EXPECT_TRUE(quic_connected()); |
+} |