| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/webtransport/quic_transport.h" |
| |
| #include <array> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/containers/span.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/test/mock_callback.h" |
| #include "mojo/public/cpp/bindings/receiver_set.h" |
| #include "services/network/public/mojom/quic_transport.mojom-blink.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/browser_interface_broker_proxy.h" |
| #include "third_party/blink/public/mojom/webtransport/quic_transport_connector.mojom-blink.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_value.h" |
| #include "third_party/blink/renderer/bindings/core/v8/to_v8_for_core.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_array_buffer.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_iterator_result_value.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_uint8_array.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_bidirectional_stream.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_quic_transport_options.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_receive_stream.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_dtls_fingerprint.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_send_stream.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_web_transport_close_info.h" |
| #include "third_party/blink/renderer/core/frame/csp/content_security_policy.h" |
| #include "third_party/blink/renderer/core/streams/readable_stream.h" |
| #include "third_party/blink/renderer/core/streams/readable_stream_default_reader.h" |
| #include "third_party/blink/renderer/core/streams/writable_stream.h" |
| #include "third_party/blink/renderer/core/streams/writable_stream_default_writer.h" |
| #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h" |
| #include "third_party/blink/renderer/core/typed_arrays/dom_typed_array.h" |
| #include "third_party/blink/renderer/modules/webtransport/receive_stream.h" |
| #include "third_party/blink/renderer/modules/webtransport/send_stream.h" |
| #include "third_party/blink/renderer/modules/webtransport/test_utils.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_code.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/heap/persistent.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/wtf/deque.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| #include "v8/include/v8.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| using ::testing::_; |
| using ::testing::ElementsAre; |
| using ::testing::Invoke; |
| using ::testing::Mock; |
| using ::testing::StrictMock; |
| using ::testing::Truly; |
| using ::testing::Unused; |
| |
| class QuicTransportConnector final |
| : public mojom::blink::QuicTransportConnector { |
| public: |
| struct ConnectArgs { |
| ConnectArgs( |
| const KURL& url, |
| Vector<network::mojom::blink::QuicTransportCertificateFingerprintPtr> |
| fingerprints, |
| mojo::PendingRemote<network::mojom::blink::QuicTransportHandshakeClient> |
| handshake_client) |
| : url(url), |
| fingerprints(std::move(fingerprints)), |
| handshake_client(std::move(handshake_client)) {} |
| |
| KURL url; |
| Vector<network::mojom::blink::QuicTransportCertificateFingerprintPtr> |
| fingerprints; |
| mojo::PendingRemote<network::mojom::blink::QuicTransportHandshakeClient> |
| handshake_client; |
| }; |
| |
| void Connect( |
| const KURL& url, |
| Vector<network::mojom::blink::QuicTransportCertificateFingerprintPtr> |
| fingerprints, |
| mojo::PendingRemote<network::mojom::blink::QuicTransportHandshakeClient> |
| handshake_client) override { |
| connect_args_.push_back( |
| ConnectArgs(url, std::move(fingerprints), std::move(handshake_client))); |
| } |
| |
| Vector<ConnectArgs> TakeConnectArgs() { return std::move(connect_args_); } |
| |
| void Bind( |
| mojo::PendingReceiver<mojom::blink::QuicTransportConnector> receiver) { |
| receiver_set_.Add(this, std::move(receiver)); |
| } |
| |
| private: |
| mojo::ReceiverSet<mojom::blink::QuicTransportConnector> receiver_set_; |
| Vector<ConnectArgs> connect_args_; |
| }; |
| |
| class MockQuicTransport : public network::mojom::blink::QuicTransport { |
| public: |
| MockQuicTransport(mojo::PendingReceiver<network::mojom::blink::QuicTransport> |
| pending_receiver) |
| : receiver_(this, std::move(pending_receiver)) {} |
| |
| MOCK_METHOD2(SendDatagram, |
| void(base::span<const uint8_t> data, |
| base::OnceCallback<void(bool)> callback)); |
| |
| MOCK_METHOD3(CreateStream, |
| void(mojo::ScopedDataPipeConsumerHandle readable, |
| mojo::ScopedDataPipeProducerHandle writable, |
| base::OnceCallback<void(bool, uint32_t)> callback)); |
| |
| MOCK_METHOD1( |
| AcceptBidirectionalStream, |
| void(base::OnceCallback<void(uint32_t, |
| mojo::ScopedDataPipeConsumerHandle, |
| mojo::ScopedDataPipeProducerHandle)>)); |
| |
| MOCK_METHOD1(AcceptUnidirectionalStream, |
| void(base::OnceCallback< |
| void(uint32_t, mojo::ScopedDataPipeConsumerHandle)>)); |
| |
| MOCK_METHOD1(SetOutgoingDatagramExpirationDuration, void(base::TimeDelta)); |
| |
| void SendFin(uint32_t stream_id) override {} |
| void AbortStream(uint32_t stream_id, uint64_t code) override {} |
| |
| private: |
| mojo::Receiver<network::mojom::blink::QuicTransport> receiver_; |
| }; |
| |
| class QuicTransportTest : public ::testing::Test { |
| public: |
| using AcceptUnidirectionalStreamCallback = |
| base::OnceCallback<void(uint32_t, mojo::ScopedDataPipeConsumerHandle)>; |
| using AcceptBidirectionalStreamCallback = |
| base::OnceCallback<void(uint32_t, |
| mojo::ScopedDataPipeConsumerHandle, |
| mojo::ScopedDataPipeProducerHandle)>; |
| |
| void AddBinder(const V8TestingScope& scope) { |
| interface_broker_ = |
| &scope.GetExecutionContext()->GetBrowserInterfaceBroker(); |
| interface_broker_->SetBinderForTesting( |
| mojom::blink::QuicTransportConnector::Name_, |
| base::BindRepeating(&QuicTransportTest::BindConnector, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| static QuicTransportOptions* EmptyOptions() { |
| return MakeGarbageCollected<QuicTransportOptions>(); |
| } |
| |
| // Creates a QuicTransport object with the given |url|. |
| QuicTransport* Create(const V8TestingScope& scope, |
| const String& url, |
| QuicTransportOptions* options) { |
| AddBinder(scope); |
| return QuicTransport::Create(scope.GetScriptState(), url, options, |
| ASSERT_NO_EXCEPTION); |
| } |
| |
| // Connects a QuicTransport object. Runs the event loop. |
| void ConnectSuccessfully(QuicTransport* quic_transport) { |
| DCHECK(!mock_quic_transport_) << "Only one connection supported, sorry"; |
| |
| test::RunPendingTasks(); |
| |
| auto args = connector_.TakeConnectArgs(); |
| if (args.size() != 1u) { |
| ADD_FAILURE() << "args.size() should be 1, but is " << args.size(); |
| return; |
| } |
| |
| mojo::Remote<network::mojom::blink::QuicTransportHandshakeClient> |
| handshake_client(std::move(args[0].handshake_client)); |
| |
| mojo::PendingRemote<network::mojom::blink::QuicTransport> |
| quic_transport_to_pass; |
| mojo::PendingRemote<network::mojom::blink::QuicTransportClient> |
| client_remote; |
| |
| mock_quic_transport_ = std::make_unique<StrictMock<MockQuicTransport>>( |
| quic_transport_to_pass.InitWithNewPipeAndPassReceiver()); |
| |
| // These are called on every connection, so expect them in every test. |
| EXPECT_CALL(*mock_quic_transport_, AcceptUnidirectionalStream(_)) |
| .WillRepeatedly([this](AcceptUnidirectionalStreamCallback callback) { |
| pending_unidirectional_accept_callbacks_.push_back( |
| std::move(callback)); |
| }); |
| |
| EXPECT_CALL(*mock_quic_transport_, AcceptBidirectionalStream(_)) |
| .WillRepeatedly([this](AcceptBidirectionalStreamCallback callback) { |
| pending_bidirectional_accept_callbacks_.push_back( |
| std::move(callback)); |
| }); |
| |
| handshake_client->OnConnectionEstablished( |
| std::move(quic_transport_to_pass), |
| client_remote.InitWithNewPipeAndPassReceiver()); |
| client_remote_.Bind(std::move(client_remote)); |
| |
| test::RunPendingTasks(); |
| } |
| |
| // Creates, connects and returns a QuicTransport object with the given |url|. |
| // Runs the event loop. |
| QuicTransport* CreateAndConnectSuccessfully( |
| const V8TestingScope& scope, |
| const String& url, |
| QuicTransportOptions* options = EmptyOptions()) { |
| auto* quic_transport = Create(scope, url, options); |
| ConnectSuccessfully(quic_transport); |
| return quic_transport; |
| } |
| |
| SendStream* CreateSendStreamSuccessfully(const V8TestingScope& scope, |
| QuicTransport* quic_transport) { |
| EXPECT_CALL(*mock_quic_transport_, CreateStream(_, _, _)) |
| .WillOnce([this](mojo::ScopedDataPipeConsumerHandle handle, Unused, |
| base::OnceCallback<void(bool, uint32_t)> callback) { |
| send_stream_consumer_handle_ = std::move(handle); |
| std::move(callback).Run(true, next_stream_id_++); |
| }); |
| |
| auto* script_state = scope.GetScriptState(); |
| ScriptPromise send_stream_promise = |
| quic_transport->createSendStream(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, send_stream_promise); |
| |
| tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(tester.IsFulfilled()); |
| auto* send_stream = V8SendStream::ToImplWithTypeCheck( |
| scope.GetIsolate(), tester.Value().V8Value()); |
| EXPECT_TRUE(send_stream); |
| return send_stream; |
| } |
| |
| mojo::ScopedDataPipeProducerHandle DoAcceptUnidirectionalStream() { |
| mojo::ScopedDataPipeProducerHandle producer; |
| mojo::ScopedDataPipeConsumerHandle consumer; |
| |
| // There's no good way to handle failure to create the pipe, so just |
| // continue. |
| CreateDataPipeForWebTransportTests(&producer, &consumer); |
| |
| std::move(pending_unidirectional_accept_callbacks_.front()) |
| .Run(next_stream_id_++, std::move(consumer)); |
| pending_unidirectional_accept_callbacks_.pop_front(); |
| |
| return producer; |
| } |
| |
| ReceiveStream* ReadReceiveStream(const V8TestingScope& scope, |
| QuicTransport* quic_transport) { |
| ReadableStream* streams = quic_transport->receiveStreams(); |
| |
| v8::Local<v8::Value> v8value = ReadValueFromStream(scope, streams); |
| |
| ReceiveStream* receive_stream = |
| V8ReceiveStream::ToImplWithTypeCheck(scope.GetIsolate(), v8value); |
| EXPECT_TRUE(receive_stream); |
| |
| return receive_stream; |
| } |
| |
| void BindConnector(mojo::ScopedMessagePipeHandle handle) { |
| connector_.Bind(mojo::PendingReceiver<mojom::blink::QuicTransportConnector>( |
| std::move(handle))); |
| } |
| |
| void TearDown() override { |
| if (!interface_broker_) |
| return; |
| interface_broker_->SetBinderForTesting( |
| mojom::blink::QuicTransportConnector::Name_, {}); |
| } |
| |
| const BrowserInterfaceBrokerProxy* interface_broker_ = nullptr; |
| WTF::Deque<AcceptUnidirectionalStreamCallback> |
| pending_unidirectional_accept_callbacks_; |
| WTF::Deque<AcceptBidirectionalStreamCallback> |
| pending_bidirectional_accept_callbacks_; |
| QuicTransportConnector connector_; |
| std::unique_ptr<MockQuicTransport> mock_quic_transport_; |
| mojo::Remote<network::mojom::blink::QuicTransportClient> client_remote_; |
| uint32_t next_stream_id_ = 0; |
| mojo::ScopedDataPipeConsumerHandle send_stream_consumer_handle_; |
| |
| base::WeakPtrFactory<QuicTransportTest> weak_ptr_factory_{this}; |
| }; |
| |
| TEST_F(QuicTransportTest, FailWithNullURL) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| QuicTransport::Create(scope.GetScriptState(), String(), EmptyOptions(), |
| exception_state); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kSyntaxError), |
| exception_state.Code()); |
| } |
| |
| TEST_F(QuicTransportTest, FailWithEmptyURL) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| QuicTransport::Create(scope.GetScriptState(), String(""), EmptyOptions(), |
| exception_state); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kSyntaxError), |
| exception_state.Code()); |
| EXPECT_EQ("The URL '' is invalid.", exception_state.Message()); |
| } |
| |
| TEST_F(QuicTransportTest, FailWithNoScheme) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| QuicTransport::Create(scope.GetScriptState(), String("no-scheme"), |
| EmptyOptions(), exception_state); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kSyntaxError), |
| exception_state.Code()); |
| EXPECT_EQ("The URL 'no-scheme' is invalid.", exception_state.Message()); |
| } |
| |
| TEST_F(QuicTransportTest, FailWithHttpsURL) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| QuicTransport::Create(scope.GetScriptState(), String("https://example.com/"), |
| EmptyOptions(), exception_state); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kSyntaxError), |
| exception_state.Code()); |
| EXPECT_EQ( |
| "The URL's scheme must be 'quic-transport'. 'https' is not allowed.", |
| exception_state.Message()); |
| } |
| |
| TEST_F(QuicTransportTest, FailWithNoHost) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| QuicTransport::Create(scope.GetScriptState(), String("quic-transport:///"), |
| EmptyOptions(), exception_state); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kSyntaxError), |
| exception_state.Code()); |
| EXPECT_EQ("The URL 'quic-transport:///' is invalid.", |
| exception_state.Message()); |
| } |
| |
| TEST_F(QuicTransportTest, FailWithURLFragment) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| QuicTransport::Create(scope.GetScriptState(), |
| String("quic-transport://example.com/#failing"), |
| EmptyOptions(), exception_state); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kSyntaxError), |
| exception_state.Code()); |
| EXPECT_EQ( |
| "The URL contains a fragment identifier ('#failing'). Fragment " |
| "identifiers are not allowed in QuicTransport URLs.", |
| exception_state.Message()); |
| } |
| |
| TEST_F(QuicTransportTest, FailByCSP) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| scope.GetExecutionContext() |
| ->GetContentSecurityPolicyForCurrentWorld() |
| ->DidReceiveHeader("connect-src 'none'", |
| *(scope.GetExecutionContext()->GetSecurityOrigin()), |
| network::mojom::ContentSecurityPolicyType::kEnforce, |
| network::mojom::ContentSecurityPolicySource::kHTTP); |
| QuicTransport::Create(scope.GetScriptState(), |
| String("quic-transport://example.com/"), EmptyOptions(), |
| exception_state); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kSecurityError), |
| exception_state.Code()); |
| EXPECT_EQ("Failed to connect to 'quic-transport://example.com/'", |
| exception_state.Message()); |
| } |
| |
| TEST_F(QuicTransportTest, PassCSP) { |
| V8TestingScope scope; |
| // This doesn't work without the https:// prefix, even thought it should |
| // according to |
| // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src. |
| auto& exception_state = scope.GetExceptionState(); |
| scope.GetExecutionContext() |
| ->GetContentSecurityPolicyForCurrentWorld() |
| ->DidReceiveHeader("connect-src quic-transport://example.com", |
| *(scope.GetExecutionContext()->GetSecurityOrigin()), |
| network::mojom::ContentSecurityPolicyType::kEnforce, |
| network::mojom::ContentSecurityPolicySource::kHTTP); |
| QuicTransport::Create(scope.GetScriptState(), |
| String("quic-transport://example.com/"), EmptyOptions(), |
| exception_state); |
| EXPECT_FALSE(exception_state.HadException()); |
| } |
| |
| TEST_F(QuicTransportTest, SendConnect) { |
| V8TestingScope scope; |
| AddBinder(scope); |
| auto* quic_transport = QuicTransport::Create( |
| scope.GetScriptState(), String("quic-transport://example.com/"), |
| EmptyOptions(), ASSERT_NO_EXCEPTION); |
| |
| test::RunPendingTasks(); |
| |
| auto args = connector_.TakeConnectArgs(); |
| ASSERT_EQ(1u, args.size()); |
| EXPECT_EQ(KURL("quic-transport://example.com/"), args[0].url); |
| EXPECT_TRUE(args[0].fingerprints.IsEmpty()); |
| EXPECT_TRUE(quic_transport->HasPendingActivity()); |
| } |
| |
| TEST_F(QuicTransportTest, SuccessfulConnect) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| ScriptPromiseTester ready_tester(scope.GetScriptState(), |
| quic_transport->ready()); |
| |
| EXPECT_TRUE(quic_transport->HasPendingActivity()); |
| |
| ready_tester.WaitUntilSettled(); |
| EXPECT_TRUE(ready_tester.IsFulfilled()); |
| } |
| |
| TEST_F(QuicTransportTest, FailedConnect) { |
| V8TestingScope scope; |
| AddBinder(scope); |
| auto* quic_transport = QuicTransport::Create( |
| scope.GetScriptState(), String("quic-transport://example.com/"), |
| EmptyOptions(), ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester ready_tester(scope.GetScriptState(), |
| quic_transport->ready()); |
| ScriptPromiseTester closed_tester(scope.GetScriptState(), |
| quic_transport->closed()); |
| |
| test::RunPendingTasks(); |
| |
| auto args = connector_.TakeConnectArgs(); |
| ASSERT_EQ(1u, args.size()); |
| |
| mojo::Remote<network::mojom::blink::QuicTransportHandshakeClient> |
| handshake_client(std::move(args[0].handshake_client)); |
| |
| handshake_client->OnHandshakeFailed(nullptr); |
| |
| test::RunPendingTasks(); |
| EXPECT_FALSE(quic_transport->HasPendingActivity()); |
| EXPECT_TRUE(ready_tester.IsRejected()); |
| EXPECT_TRUE(closed_tester.IsRejected()); |
| } |
| |
| TEST_F(QuicTransportTest, SendConnectWithFingerprint) { |
| V8TestingScope scope; |
| AddBinder(scope); |
| auto* fingerprints = MakeGarbageCollected<RTCDtlsFingerprint>(); |
| fingerprints->setAlgorithm("sha-256"); |
| fingerprints->setValue( |
| "ED:3D:D7:C3:67:10:94:68:D1:DC:D1:26:5C:B2:74:D7:1C:A2:63:3E:94:94:C0:84:" |
| "39:D6:64:FA:08:B9:77:37"); |
| auto* options = MakeGarbageCollected<QuicTransportOptions>(); |
| options->setServerCertificateFingerprints({fingerprints}); |
| QuicTransport::Create(scope.GetScriptState(), |
| String("quic-transport://example.com/"), options, |
| ASSERT_NO_EXCEPTION); |
| |
| test::RunPendingTasks(); |
| |
| auto args = connector_.TakeConnectArgs(); |
| ASSERT_EQ(1u, args.size()); |
| ASSERT_EQ(1u, args[0].fingerprints.size()); |
| EXPECT_EQ(args[0].fingerprints[0]->algorithm, "sha-256"); |
| EXPECT_EQ(args[0].fingerprints[0]->fingerprint, |
| "ED:3D:D7:C3:67:10:94:68:D1:DC:D1:26:5C:B2:74:D7:1C:A2:63:3E:94:94:" |
| "C0:84:39:D6:64:FA:08:B9:77:37"); |
| } |
| |
| TEST_F(QuicTransportTest, CloseDuringConnect) { |
| V8TestingScope scope; |
| AddBinder(scope); |
| auto* quic_transport = QuicTransport::Create( |
| scope.GetScriptState(), String("quic-transport://example.com/"), |
| EmptyOptions(), ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester ready_tester(scope.GetScriptState(), |
| quic_transport->ready()); |
| ScriptPromiseTester closed_tester(scope.GetScriptState(), |
| quic_transport->closed()); |
| |
| test::RunPendingTasks(); |
| |
| auto args = connector_.TakeConnectArgs(); |
| ASSERT_EQ(1u, args.size()); |
| |
| quic_transport->close(nullptr); |
| |
| test::RunPendingTasks(); |
| |
| EXPECT_FALSE(quic_transport->HasPendingActivity()); |
| EXPECT_TRUE(ready_tester.IsRejected()); |
| EXPECT_TRUE(closed_tester.IsFulfilled()); |
| } |
| |
| TEST_F(QuicTransportTest, CloseAfterConnection) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| ScriptPromiseTester ready_tester(scope.GetScriptState(), |
| quic_transport->ready()); |
| ScriptPromiseTester closed_tester(scope.GetScriptState(), |
| quic_transport->closed()); |
| |
| WebTransportCloseInfo close_info; |
| close_info.setErrorCode(42); |
| close_info.setReason("because"); |
| quic_transport->close(&close_info); |
| |
| test::RunPendingTasks(); |
| |
| // TODO(ricea): Check that the close info is sent through correctly, once we |
| // start sending it. |
| |
| EXPECT_FALSE(quic_transport->HasPendingActivity()); |
| EXPECT_TRUE(ready_tester.IsFulfilled()); |
| EXPECT_TRUE(closed_tester.IsFulfilled()); |
| |
| // Calling close again does nothing. |
| quic_transport->close(nullptr); |
| } |
| |
| // A live connection will be kept alive even if there is no explicit reference. |
| // When the underlying connection is shut down, the connection will be swept. |
| TEST_F(QuicTransportTest, GarbageCollection) { |
| V8TestingScope scope; |
| |
| WeakPersistent<QuicTransport> quic_transport; |
| |
| { |
| // The streams created when creating a QuicTransport create some v8 handles. |
| // To ensure these are collected, we need to create a handle scope. This is |
| // not a problem for garbage collection in normal operation. |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| } |
| |
| // Pretend the stack is empty. This will avoid accidentally treating any |
| // copies of the |quic_transport| pointer as references. |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_TRUE(quic_transport); |
| |
| quic_transport->close(nullptr); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(quic_transport); |
| } |
| |
| TEST_F(QuicTransportTest, GarbageCollectMojoConnectionError) { |
| V8TestingScope scope; |
| |
| WeakPersistent<QuicTransport> quic_transport; |
| |
| { |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| } |
| |
| ScriptPromiseTester closed_tester(scope.GetScriptState(), |
| quic_transport->closed()); |
| |
| // Closing the server-side of the pipe causes a mojo connection error. |
| client_remote_.reset(); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(quic_transport); |
| EXPECT_TRUE(closed_tester.IsRejected()); |
| } |
| |
| TEST_F(QuicTransportTest, SendDatagram) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| EXPECT_CALL(*mock_quic_transport_, SendDatagram(ElementsAre('A'), _)) |
| .WillOnce(Invoke([](base::span<const uint8_t>, |
| MockQuicTransport::SendDatagramCallback callback) { |
| std::move(callback).Run(true); |
| })); |
| |
| auto* writable = quic_transport->sendDatagrams(); |
| auto* script_state = scope.GetScriptState(); |
| auto* writer = writable->getWriter(script_state, ASSERT_NO_EXCEPTION); |
| auto* chunk = DOMUint8Array::Create(1); |
| *chunk->Data() = 'A'; |
| ScriptPromise result = |
| writer->write(script_state, ScriptValue::From(script_state, chunk), |
| ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, result); |
| tester.WaitUntilSettled(); |
| EXPECT_TRUE(tester.IsFulfilled()); |
| EXPECT_TRUE(tester.Value().IsUndefined()); |
| } |
| |
| TEST_F(QuicTransportTest, BackpressureForOutgoingDatagrams) { |
| V8TestingScope scope; |
| auto* const options = MakeGarbageCollected<QuicTransportOptions>(); |
| options->setDatagramWritableHighWaterMark(3); |
| auto* quic_transport = CreateAndConnectSuccessfully( |
| scope, "quic-transport://example.com", options); |
| |
| EXPECT_CALL(*mock_quic_transport_, SendDatagram(_, _)) |
| .Times(4) |
| .WillRepeatedly( |
| Invoke([](base::span<const uint8_t>, |
| MockQuicTransport::SendDatagramCallback callback) { |
| std::move(callback).Run(true); |
| })); |
| |
| auto* writable = quic_transport->sendDatagrams(); |
| auto* script_state = scope.GetScriptState(); |
| auto* writer = writable->getWriter(script_state, ASSERT_NO_EXCEPTION); |
| |
| ScriptPromise promise1; |
| ScriptPromise promise2; |
| ScriptPromise promise3; |
| ScriptPromise promise4; |
| |
| { |
| auto* chunk = DOMUint8Array::Create(1); |
| *chunk->Data() = 'A'; |
| promise1 = |
| writer->write(script_state, ScriptValue::From(script_state, chunk), |
| ASSERT_NO_EXCEPTION); |
| } |
| { |
| auto* chunk = DOMUint8Array::Create(1); |
| *chunk->Data() = 'B'; |
| promise2 = |
| writer->write(script_state, ScriptValue::From(script_state, chunk), |
| ASSERT_NO_EXCEPTION); |
| } |
| { |
| auto* chunk = DOMUint8Array::Create(1); |
| *chunk->Data() = 'C'; |
| promise3 = |
| writer->write(script_state, ScriptValue::From(script_state, chunk), |
| ASSERT_NO_EXCEPTION); |
| } |
| { |
| auto* chunk = DOMUint8Array::Create(1); |
| *chunk->Data() = 'D'; |
| promise4 = |
| writer->write(script_state, ScriptValue::From(script_state, chunk), |
| ASSERT_NO_EXCEPTION); |
| } |
| |
| // The first two promises are resolved immediately. |
| v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate()); |
| EXPECT_EQ(promise1.V8Promise()->State(), v8::Promise::kFulfilled); |
| EXPECT_EQ(promise2.V8Promise()->State(), v8::Promise::kFulfilled); |
| EXPECT_EQ(promise3.V8Promise()->State(), v8::Promise::kPending); |
| EXPECT_EQ(promise4.V8Promise()->State(), v8::Promise::kPending); |
| |
| // The rest are resolved by the callback. |
| test::RunPendingTasks(); |
| v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate()); |
| EXPECT_EQ(promise3.V8Promise()->State(), v8::Promise::kFulfilled); |
| EXPECT_EQ(promise4.V8Promise()->State(), v8::Promise::kFulfilled); |
| } |
| |
| TEST_F(QuicTransportTest, SendDatagramBeforeConnect) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| Create(scope, "quic-transport://example.com", EmptyOptions()); |
| |
| auto* writable = quic_transport->sendDatagrams(); |
| auto* script_state = scope.GetScriptState(); |
| auto* writer = writable->getWriter(script_state, ASSERT_NO_EXCEPTION); |
| auto* chunk = DOMUint8Array::Create(1); |
| *chunk->Data() = 'A'; |
| ScriptPromise result = |
| writer->write(script_state, ScriptValue::From(script_state, chunk), |
| ASSERT_NO_EXCEPTION); |
| |
| ConnectSuccessfully(quic_transport); |
| |
| // No datagram is sent. |
| |
| ScriptPromiseTester tester(script_state, result); |
| tester.WaitUntilSettled(); |
| EXPECT_TRUE(tester.IsFulfilled()); |
| EXPECT_TRUE(tester.Value().IsUndefined()); |
| } |
| |
| TEST_F(QuicTransportTest, SendDatagramAfterClose) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| quic_transport->close(nullptr); |
| test::RunPendingTasks(); |
| |
| auto* writable = quic_transport->sendDatagrams(); |
| auto* script_state = scope.GetScriptState(); |
| auto* writer = writable->getWriter(script_state, ASSERT_NO_EXCEPTION); |
| |
| auto* chunk = DOMUint8Array::Create(1); |
| *chunk->Data() = 'A'; |
| ScriptPromise result = |
| writer->write(script_state, ScriptValue::From(script_state, chunk), |
| ASSERT_NO_EXCEPTION); |
| |
| // No datagram is sent. |
| |
| ScriptPromiseTester tester(script_state, result); |
| tester.WaitUntilSettled(); |
| EXPECT_TRUE(tester.IsRejected()); |
| } |
| |
| Vector<uint8_t> GetValueAsVector(ScriptState* script_state, |
| ScriptValue iterator_result) { |
| bool done = false; |
| v8::Local<v8::Value> value; |
| if (!V8UnpackIteratorResult(script_state, |
| iterator_result.V8Value().As<v8::Object>(), &done) |
| .ToLocal(&value)) { |
| ADD_FAILURE() << "unable to unpack iterator_result"; |
| return {}; |
| } |
| |
| EXPECT_FALSE(done); |
| auto* array = |
| V8Uint8Array::ToImplWithTypeCheck(script_state->GetIsolate(), value); |
| if (!array) { |
| ADD_FAILURE() << "value was not a Uint8Array"; |
| return {}; |
| } |
| |
| Vector<uint8_t> result; |
| result.Append(array->Data(), array->length()); |
| return result; |
| } |
| |
| TEST_F(QuicTransportTest, ReceiveDatagramBeforeRead) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| const std::array<uint8_t, 1> chunk = {'A'}; |
| client_remote_->OnDatagramReceived(chunk); |
| |
| test::RunPendingTasks(); |
| |
| auto* readable = quic_transport->receiveDatagrams(); |
| auto* script_state = scope.GetScriptState(); |
| auto* reader = |
| readable->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromise result = reader->read(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, result); |
| tester.WaitUntilSettled(); |
| EXPECT_TRUE(tester.IsFulfilled()); |
| |
| EXPECT_THAT(GetValueAsVector(script_state, tester.Value()), ElementsAre('A')); |
| } |
| |
| TEST_F(QuicTransportTest, ReceiveDatagramDuringRead) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| auto* readable = quic_transport->receiveDatagrams(); |
| auto* script_state = scope.GetScriptState(); |
| auto* reader = |
| readable->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromise result = reader->read(script_state, ASSERT_NO_EXCEPTION); |
| |
| const std::array<uint8_t, 1> chunk = {'A'}; |
| client_remote_->OnDatagramReceived(chunk); |
| |
| ScriptPromiseTester tester(script_state, result); |
| tester.WaitUntilSettled(); |
| EXPECT_TRUE(tester.IsFulfilled()); |
| |
| EXPECT_THAT(GetValueAsVector(script_state, tester.Value()), ElementsAre('A')); |
| } |
| |
| // This test documents the current behaviour. If you improve the behaviour, |
| // change the test! |
| TEST_F(QuicTransportTest, DatagramsAreDropped) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| // Chunk 'A' gets placed in the readable queue. |
| const std::array<uint8_t, 1> chunk1 = {'A'}; |
| client_remote_->OnDatagramReceived(chunk1); |
| |
| // Chunk 'B' gets dropped, because there is no space in the readable queue. |
| const std::array<uint8_t, 1> chunk2 = {'B'}; |
| client_remote_->OnDatagramReceived(chunk2); |
| |
| // Make sure that the calls have run. |
| test::RunPendingTasks(); |
| |
| auto* readable = quic_transport->receiveDatagrams(); |
| auto* script_state = scope.GetScriptState(); |
| auto* reader = |
| readable->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromise result1 = reader->read(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromise result2 = reader->read(script_state, ASSERT_NO_EXCEPTION); |
| |
| ScriptPromiseTester tester1(script_state, result1); |
| ScriptPromiseTester tester2(script_state, result2); |
| tester1.WaitUntilSettled(); |
| EXPECT_TRUE(tester1.IsFulfilled()); |
| EXPECT_FALSE(tester2.IsFulfilled()); |
| |
| EXPECT_THAT(GetValueAsVector(script_state, tester1.Value()), |
| ElementsAre('A')); |
| |
| // Chunk 'C' fulfills the pending read. |
| const std::array<uint8_t, 1> chunk3 = {'C'}; |
| client_remote_->OnDatagramReceived(chunk3); |
| |
| tester2.WaitUntilSettled(); |
| EXPECT_TRUE(tester2.IsFulfilled()); |
| |
| EXPECT_THAT(GetValueAsVector(script_state, tester2.Value()), |
| ElementsAre('C')); |
| } |
| |
| bool ValidProducerHandle(const mojo::ScopedDataPipeProducerHandle& handle) { |
| return handle.is_valid(); |
| } |
| |
| bool ValidConsumerHandle(const mojo::ScopedDataPipeConsumerHandle& handle) { |
| return handle.is_valid(); |
| } |
| |
| TEST_F(QuicTransportTest, CreateSendStream) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| EXPECT_CALL(*mock_quic_transport_, |
| CreateStream(Truly(ValidConsumerHandle), |
| Not(Truly(ValidProducerHandle)), _)) |
| .WillOnce([](Unused, Unused, |
| base::OnceCallback<void(bool, uint32_t)> callback) { |
| std::move(callback).Run(true, 0); |
| }); |
| |
| auto* script_state = scope.GetScriptState(); |
| ScriptPromise send_stream_promise = |
| quic_transport->createSendStream(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, send_stream_promise); |
| |
| tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(tester.IsFulfilled()); |
| auto* send_stream = V8SendStream::ToImplWithTypeCheck( |
| scope.GetIsolate(), tester.Value().V8Value()); |
| EXPECT_TRUE(send_stream); |
| } |
| |
| TEST_F(QuicTransportTest, CreateSendStreamBeforeConnect) { |
| V8TestingScope scope; |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* quic_transport = |
| QuicTransport::Create(script_state, "quic-transport://example.com", |
| EmptyOptions(), ASSERT_NO_EXCEPTION); |
| auto& exception_state = scope.GetExceptionState(); |
| ScriptPromise send_stream_promise = |
| quic_transport->createSendStream(script_state, exception_state); |
| EXPECT_TRUE(send_stream_promise.IsEmpty()); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(static_cast<int>(DOMExceptionCode::kNetworkError), |
| exception_state.Code()); |
| } |
| |
| TEST_F(QuicTransportTest, CreateSendStreamFailure) { |
| V8TestingScope scope; |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| EXPECT_CALL(*mock_quic_transport_, CreateStream(_, _, _)) |
| .WillOnce([](Unused, Unused, |
| base::OnceCallback<void(bool, uint32_t)> callback) { |
| std::move(callback).Run(false, 0); |
| }); |
| |
| auto* script_state = scope.GetScriptState(); |
| ScriptPromise send_stream_promise = |
| quic_transport->createSendStream(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, send_stream_promise); |
| |
| tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(tester.IsRejected()); |
| DOMException* exception = V8DOMException::ToImplWithTypeCheck( |
| scope.GetIsolate(), tester.Value().V8Value()); |
| EXPECT_EQ(exception->name(), "NetworkError"); |
| EXPECT_EQ(exception->message(), "Failed to create send stream."); |
| } |
| |
| // Every active stream is kept alive by the QuicTransport object. |
| TEST_F(QuicTransportTest, SendStreamGarbageCollection) { |
| V8TestingScope scope; |
| |
| WeakPersistent<QuicTransport> quic_transport; |
| WeakPersistent<SendStream> send_stream; |
| |
| { |
| // The streams created when creating a QuicTransport or SendStream create |
| // some v8 handles. To ensure these are collected, we need to create a |
| // handle scope. This is not a problem for garbage collection in normal |
| // operation. |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| |
| quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| send_stream = CreateSendStreamSuccessfully(scope, quic_transport); |
| } |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_TRUE(quic_transport); |
| EXPECT_TRUE(send_stream); |
| |
| quic_transport->close(nullptr); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(quic_transport); |
| EXPECT_FALSE(send_stream); |
| } |
| |
| // A live stream will be kept alive even if there is no explicit reference. |
| // When the underlying connection is shut down, the connection will be swept. |
| TEST_F(QuicTransportTest, SendStreamGarbageCollectionLocalClose) { |
| V8TestingScope scope; |
| |
| WeakPersistent<SendStream> send_stream; |
| |
| { |
| // The writable stream created when creating a SendStream creates some |
| // v8 handles. To ensure these are collected, we need to create a handle |
| // scope. This is not a problem for garbage collection in normal operation. |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| send_stream = CreateSendStreamSuccessfully(scope, quic_transport); |
| } |
| |
| // Pretend the stack is empty. This will avoid accidentally treating any |
| // copies of the |send_stream| pointer as references. |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| ASSERT_TRUE(send_stream); |
| |
| auto* script_state = scope.GetScriptState(); |
| |
| ScriptPromise close_promise = |
| send_stream->writable()->close(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, close_promise); |
| tester.WaitUntilSettled(); |
| EXPECT_TRUE(tester.IsFulfilled()); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(send_stream); |
| } |
| |
| TEST_F(QuicTransportTest, SendStreamGarbageCollectionRemoteClose) { |
| V8TestingScope scope; |
| |
| WeakPersistent<SendStream> send_stream; |
| |
| { |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| send_stream = CreateSendStreamSuccessfully(scope, quic_transport); |
| } |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| ASSERT_TRUE(send_stream); |
| |
| // Close the other end of the pipe. |
| send_stream_consumer_handle_.reset(); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(send_stream); |
| } |
| |
| // A live stream will be kept alive even if there is no explicit reference. |
| // When the underlying connection is shut down, the connection will be swept. |
| TEST_F(QuicTransportTest, ReceiveStreamGarbageCollectionCancel) { |
| V8TestingScope scope; |
| |
| WeakPersistent<ReceiveStream> receive_stream; |
| mojo::ScopedDataPipeProducerHandle producer; |
| |
| { |
| // The readable stream created when creating a ReceiveStream creates some |
| // v8 handles. To ensure these are collected, we need to create a handle |
| // scope. This is not a problem for garbage collection in normal operation. |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| producer = DoAcceptUnidirectionalStream(); |
| receive_stream = ReadReceiveStream(scope, quic_transport); |
| } |
| |
| // Pretend the stack is empty. This will avoid accidentally treating any |
| // copies of the |receive_stream| pointer as references. |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| ASSERT_TRUE(receive_stream); |
| |
| auto* script_state = scope.GetScriptState(); |
| |
| ScriptPromise cancel_promise; |
| { |
| // Cancelling also creates v8 handles, so we need a new handle scope as |
| // above. |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| cancel_promise = |
| receive_stream->readable()->cancel(script_state, ASSERT_NO_EXCEPTION); |
| } |
| |
| ScriptPromiseTester tester(script_state, cancel_promise); |
| tester.WaitUntilSettled(); |
| EXPECT_TRUE(tester.IsFulfilled()); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(receive_stream); |
| } |
| |
| TEST_F(QuicTransportTest, ReceiveStreamGarbageCollectionRemoteClose) { |
| V8TestingScope scope; |
| |
| WeakPersistent<ReceiveStream> receive_stream; |
| mojo::ScopedDataPipeProducerHandle producer; |
| |
| { |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| producer = DoAcceptUnidirectionalStream(); |
| receive_stream = ReadReceiveStream(scope, quic_transport); |
| } |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| ASSERT_TRUE(receive_stream); |
| |
| // Close the other end of the pipe. |
| producer.reset(); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| ASSERT_TRUE(receive_stream); |
| |
| receive_stream->OnIncomingStreamClosed(false); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(receive_stream); |
| } |
| |
| // This is the same test as ReceiveStreamGarbageCollectionRemoteClose, except |
| // that the order of the data pipe being reset and the OnIncomingStreamClosed |
| // message is reversed. It is important that the object is not collected until |
| // both events have happened. |
| TEST_F(QuicTransportTest, ReceiveStreamGarbageCollectionRemoteCloseReverse) { |
| V8TestingScope scope; |
| |
| WeakPersistent<ReceiveStream> receive_stream; |
| mojo::ScopedDataPipeProducerHandle producer; |
| |
| { |
| v8::HandleScope handle_scope(scope.GetIsolate()); |
| |
| QuicTransport* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| producer = DoAcceptUnidirectionalStream(); |
| receive_stream = ReadReceiveStream(scope, quic_transport); |
| } |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| ASSERT_TRUE(receive_stream); |
| |
| receive_stream->OnIncomingStreamClosed(false); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| ASSERT_TRUE(receive_stream); |
| |
| producer.reset(); |
| |
| test::RunPendingTasks(); |
| |
| ThreadState::Current()->CollectAllGarbageForTesting(); |
| |
| EXPECT_FALSE(receive_stream); |
| } |
| |
| TEST_F(QuicTransportTest, CreateSendStreamAbortedByClose) { |
| V8TestingScope scope; |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| base::OnceCallback<void(bool, uint32_t)> create_stream_callback; |
| EXPECT_CALL(*mock_quic_transport_, CreateStream(_, _, _)) |
| .WillOnce([&](Unused, Unused, |
| base::OnceCallback<void(bool, uint32_t)> callback) { |
| create_stream_callback = std::move(callback); |
| }); |
| |
| ScriptPromise send_stream_promise = |
| quic_transport->createSendStream(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, send_stream_promise); |
| |
| test::RunPendingTasks(); |
| |
| quic_transport->close(nullptr); |
| std::move(create_stream_callback).Run(true, 0); |
| |
| tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(tester.IsRejected()); |
| } |
| |
| // ReceiveStream functionality is thoroughly tested in incoming_stream_test.cc. |
| // This test just verifies that the creation is done correctly. |
| TEST_F(QuicTransportTest, CreateReceiveStream) { |
| V8TestingScope scope; |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| mojo::ScopedDataPipeProducerHandle producer = DoAcceptUnidirectionalStream(); |
| |
| ReceiveStream* receive_stream = ReadReceiveStream(scope, quic_transport); |
| |
| const char data[] = "what"; |
| uint32_t num_bytes = 4u; |
| |
| EXPECT_EQ( |
| producer->WriteData(data, &num_bytes, MOJO_WRITE_DATA_FLAG_ALL_OR_NONE), |
| MOJO_RESULT_OK); |
| EXPECT_EQ(num_bytes, 4u); |
| |
| producer.reset(); |
| quic_transport->OnIncomingStreamClosed(/*stream_id=*/0, true); |
| |
| auto* reader = receive_stream->readable()->GetDefaultReaderForTesting( |
| script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromise read_promise = reader->read(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester read_tester(script_state, read_promise); |
| read_tester.WaitUntilSettled(); |
| EXPECT_TRUE(read_tester.IsFulfilled()); |
| auto read_result = read_tester.Value().V8Value(); |
| ASSERT_TRUE(read_result->IsObject()); |
| v8::Local<v8::Value> value; |
| bool done = false; |
| ASSERT_TRUE( |
| V8UnpackIteratorResult(script_state, read_result.As<v8::Object>(), &done) |
| .ToLocal(&value)); |
| DOMUint8Array* u8array = |
| V8Uint8Array::ToImplWithTypeCheck(scope.GetIsolate(), value); |
| ASSERT_TRUE(u8array); |
| EXPECT_THAT(base::make_span(static_cast<uint8_t*>(u8array->Data()), |
| u8array->byteLength()), |
| ElementsAre('w', 'h', 'a', 't')); |
| } |
| |
| TEST_F(QuicTransportTest, CreateReceiveStreamThenClose) { |
| V8TestingScope scope; |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| mojo::ScopedDataPipeProducerHandle producer = DoAcceptUnidirectionalStream(); |
| |
| ReceiveStream* receive_stream = ReadReceiveStream(scope, quic_transport); |
| |
| auto* reader = receive_stream->readable()->GetDefaultReaderForTesting( |
| script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromise read_promise = reader->read(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester read_tester(script_state, read_promise); |
| |
| quic_transport->close(nullptr); |
| |
| read_tester.WaitUntilSettled(); |
| EXPECT_TRUE(read_tester.IsRejected()); |
| DOMException* exception = V8DOMException::ToImplWithTypeCheck( |
| scope.GetIsolate(), read_tester.Value().V8Value()); |
| ASSERT_TRUE(exception); |
| EXPECT_EQ(exception->code(), |
| static_cast<uint16_t>(DOMExceptionCode::kNetworkError)); |
| |
| // TODO(ricea): Fix this message if possible. |
| EXPECT_EQ(exception->message(), |
| "The stream was aborted by the remote server"); |
| } |
| |
| TEST_F(QuicTransportTest, CreateReceiveStreamThenRemoteClose) { |
| V8TestingScope scope; |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| mojo::ScopedDataPipeProducerHandle producer = DoAcceptUnidirectionalStream(); |
| |
| ReceiveStream* receive_stream = ReadReceiveStream(scope, quic_transport); |
| |
| auto* reader = receive_stream->readable()->GetDefaultReaderForTesting( |
| script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromise read_promise = reader->read(script_state, ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester read_tester(script_state, read_promise); |
| |
| client_remote_.reset(); |
| |
| read_tester.WaitUntilSettled(); |
| EXPECT_TRUE(read_tester.IsRejected()); |
| DOMException* exception = V8DOMException::ToImplWithTypeCheck( |
| scope.GetIsolate(), read_tester.Value().V8Value()); |
| ASSERT_TRUE(exception); |
| EXPECT_EQ(exception->code(), |
| static_cast<uint16_t>(DOMExceptionCode::kNetworkError)); |
| |
| // TODO(ricea): Fix this message if possible. |
| EXPECT_EQ(exception->message(), |
| "The stream was aborted by the remote server"); |
| } |
| |
| // BidirectionalStreams are thoroughly tested in bidirectional_stream_test.cc. |
| // Here we just test the QuicTransport APIs. |
| TEST_F(QuicTransportTest, CreateBidirectionalStream) { |
| V8TestingScope scope; |
| |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| EXPECT_CALL( |
| *mock_quic_transport_, |
| CreateStream(Truly(ValidConsumerHandle), Truly(ValidProducerHandle), _)) |
| .WillOnce([](Unused, Unused, |
| base::OnceCallback<void(bool, uint32_t)> callback) { |
| std::move(callback).Run(true, 0); |
| }); |
| |
| auto* script_state = scope.GetScriptState(); |
| ScriptPromise bidirectional_stream_promise = |
| quic_transport->createBidirectionalStream(script_state, |
| ASSERT_NO_EXCEPTION); |
| ScriptPromiseTester tester(script_state, bidirectional_stream_promise); |
| |
| tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(tester.IsFulfilled()); |
| auto* bidirectional_stream = V8BidirectionalStream::ToImplWithTypeCheck( |
| scope.GetIsolate(), tester.Value().V8Value()); |
| EXPECT_TRUE(bidirectional_stream); |
| } |
| |
| TEST_F(QuicTransportTest, ReceiveBidirectionalStream) { |
| V8TestingScope scope; |
| |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| mojo::ScopedDataPipeProducerHandle outgoing_producer; |
| mojo::ScopedDataPipeConsumerHandle outgoing_consumer; |
| ASSERT_TRUE(CreateDataPipeForWebTransportTests(&outgoing_producer, |
| &outgoing_consumer)); |
| |
| mojo::ScopedDataPipeProducerHandle incoming_producer; |
| mojo::ScopedDataPipeConsumerHandle incoming_consumer; |
| ASSERT_TRUE(CreateDataPipeForWebTransportTests(&incoming_producer, |
| &incoming_consumer)); |
| |
| std::move(pending_bidirectional_accept_callbacks_.front()) |
| .Run(next_stream_id_++, std::move(incoming_consumer), |
| std::move(outgoing_producer)); |
| |
| ReadableStream* streams = quic_transport->receiveBidirectionalStreams(); |
| |
| v8::Local<v8::Value> v8value = ReadValueFromStream(scope, streams); |
| |
| BidirectionalStream* bidirectional_stream = |
| V8BidirectionalStream::ToImplWithTypeCheck(scope.GetIsolate(), v8value); |
| EXPECT_TRUE(bidirectional_stream); |
| } |
| |
| TEST_F(QuicTransportTest, SetDatagramWritableQueueExpirationDuration) { |
| V8TestingScope scope; |
| |
| auto* quic_transport = |
| CreateAndConnectSuccessfully(scope, "quic-transport://example.com"); |
| |
| constexpr base::TimeDelta duration = base::TimeDelta::FromMilliseconds(40); |
| EXPECT_CALL(*mock_quic_transport_, |
| SetOutgoingDatagramExpirationDuration(duration)); |
| |
| quic_transport->SetDatagramWritableQueueExpirationDuration(duration); |
| |
| test::RunPendingTasks(); |
| } |
| |
| } // namespace |
| |
| } // namespace blink |