| // 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. |
| |
| // Most testing for WebSocketStream is done via web platform tests. These unit |
| // tests just cover the most common functionality. |
| |
| #include "third_party/blink/renderer/modules/websockets/websocket_stream.h" |
| |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/devtools/console_message.mojom-blink.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise.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/v8_binding_for_testing.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_websocket_close_info.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_websocket_stream_options.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/modules/websockets/mock_websocket_channel.h" |
| #include "third_party/blink/renderer/modules/websockets/websocket_channel.h" |
| #include "third_party/blink/renderer/modules/websockets/websocket_channel_client.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_code.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_binding.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| using ::testing::_; |
| using ::testing::InSequence; |
| using ::testing::Return; |
| |
| typedef testing::StrictMock<testing::MockFunction<void(int)>> |
| Checkpoint; // NOLINT |
| |
| class WebSocketStreamTest : public ::testing::Test { |
| public: |
| WebSocketStreamTest() |
| : channel_(MakeGarbageCollected<MockWebSocketChannel>()) {} |
| |
| void TearDown() override { |
| testing::Mock::VerifyAndClear(channel_); |
| channel_ = nullptr; |
| } |
| |
| // Returns a reference for easy use with EXPECT_CALL(Channel(), ...). |
| MockWebSocketChannel& Channel() const { return *channel_; } |
| |
| WebSocketStream* Create(ScriptState* script_state, |
| const String& url, |
| ExceptionState& exception_state) { |
| return Create(script_state, url, WebSocketStreamOptions::Create(), |
| exception_state); |
| } |
| |
| WebSocketStream* Create(ScriptState* script_state, |
| const String& url, |
| WebSocketStreamOptions* options, |
| ExceptionState& exception_state) { |
| return WebSocketStream::CreateForTesting(script_state, url, options, |
| channel_, exception_state); |
| } |
| |
| bool IsDOMException(ScriptState* script_state, |
| ScriptValue value, |
| DOMExceptionCode code) { |
| auto* dom_exception = V8DOMException::ToImplWithTypeCheck( |
| script_state->GetIsolate(), value.V8Value()); |
| if (!dom_exception) |
| return false; |
| |
| return dom_exception->code() == static_cast<uint16_t>(code); |
| } |
| |
| // Returns the value of the property |key| on object |object|, stringified as |
| // a UTF-8 encoded std::string so that it can be compared and printed by |
| // EXPECT_EQ. |object| must have been verified to be a v8::Object. |key| must |
| // be encoded as latin1. undefined and null values are stringified as |
| // "undefined" and "null" respectively. "undefined" is also used to mean "not |
| // found". |
| std::string PropertyAsString(ScriptState* script_state, |
| v8::Local<v8::Value> object, |
| String key) { |
| v8::Local<v8::Value> value; |
| auto* isolate = script_state->GetIsolate(); |
| if (!object.As<v8::Object>() |
| ->GetRealNamedProperty(script_state->GetContext(), |
| V8String(isolate, key)) |
| .ToLocal(&value)) { |
| value = v8::Undefined(isolate); |
| } |
| |
| v8::String::Utf8Value utf8value(isolate, value); |
| return std::string(*utf8value, utf8value.length()); |
| } |
| |
| private: |
| Persistent<MockWebSocketChannel> channel_; |
| }; |
| |
| TEST_F(WebSocketStreamTest, ConstructWithBadURL) { |
| V8TestingScope scope; |
| auto& exception_state = scope.GetExceptionState(); |
| |
| EXPECT_CALL(Channel(), ApplyBackpressure()); |
| |
| auto* stream = Create(scope.GetScriptState(), "bad-scheme:", exception_state); |
| |
| EXPECT_FALSE(stream); |
| EXPECT_TRUE(exception_state.HadException()); |
| EXPECT_EQ(DOMExceptionCode::kSyntaxError, |
| exception_state.CodeAs<DOMExceptionCode>()); |
| EXPECT_EQ( |
| "The URL's scheme must be either 'ws' or 'wss'. 'bad-scheme' is not " |
| "allowed.", |
| exception_state.Message()); |
| } |
| |
| // Most coverage for bad constructor arguments is provided by |
| // dom_websocket_test.cc. |
| // TODO(ricea): Should we duplicate those tests here? |
| |
| TEST_F(WebSocketStreamTest, Connect) { |
| V8TestingScope scope; |
| |
| { |
| InSequence s; |
| EXPECT_CALL(Channel(), ApplyBackpressure()); |
| EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/hoge"), String())) |
| .WillOnce(Return(true)); |
| } |
| |
| auto* stream = Create(scope.GetScriptState(), "ws://example.com/hoge", |
| ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(stream); |
| EXPECT_EQ(KURL("ws://example.com/hoge"), stream->url()); |
| } |
| |
| TEST_F(WebSocketStreamTest, ConnectWithProtocols) { |
| V8TestingScope scope; |
| |
| { |
| InSequence s; |
| EXPECT_CALL(Channel(), ApplyBackpressure()); |
| EXPECT_CALL(Channel(), |
| Connect(KURL("ws://example.com/chat"), String("chat0, chat1"))) |
| .WillOnce(Return(true)); |
| } |
| |
| auto* options = WebSocketStreamOptions::Create(); |
| options->setProtocols({"chat0", "chat1"}); |
| auto* stream = Create(scope.GetScriptState(), "ws://example.com/chat", |
| options, ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(stream); |
| EXPECT_EQ(KURL("ws://example.com/chat"), stream->url()); |
| } |
| |
| TEST_F(WebSocketStreamTest, ConnectWithFailedHandshake) { |
| V8TestingScope scope; |
| |
| { |
| InSequence s; |
| EXPECT_CALL(Channel(), ApplyBackpressure()); |
| EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/chat"), String())) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(Channel(), Disconnect()); |
| } |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* stream = |
| Create(script_state, "ws://example.com/chat", ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(stream); |
| EXPECT_EQ(KURL("ws://example.com/chat"), stream->url()); |
| |
| ScriptPromiseTester connection_tester(script_state, |
| stream->connection(script_state)); |
| ScriptPromiseTester closed_tester(script_state, stream->closed(script_state)); |
| |
| stream->DidError(); |
| stream->DidClose(WebSocketChannelClient::kClosingHandshakeIncomplete, |
| WebSocketChannel::kCloseEventCodeAbnormalClosure, String()); |
| |
| connection_tester.WaitUntilSettled(); |
| closed_tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(connection_tester.IsRejected()); |
| EXPECT_TRUE(IsDOMException(script_state, connection_tester.Value(), |
| DOMExceptionCode::kNetworkError)); |
| EXPECT_TRUE(closed_tester.IsRejected()); |
| EXPECT_TRUE(IsDOMException(script_state, closed_tester.Value(), |
| DOMExceptionCode::kNetworkError)); |
| } |
| |
| TEST_F(WebSocketStreamTest, ConnectWithSuccessfulHandshake) { |
| V8TestingScope scope; |
| |
| { |
| InSequence s; |
| EXPECT_CALL(Channel(), ApplyBackpressure()); |
| EXPECT_CALL(Channel(), |
| Connect(KURL("ws://example.com/chat"), String("chat"))) |
| .WillOnce(Return(true)); |
| } |
| |
| auto* options = WebSocketStreamOptions::Create(); |
| options->setProtocols({"chat"}); |
| auto* script_state = scope.GetScriptState(); |
| auto* stream = Create(script_state, "ws://example.com/chat", options, |
| ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(stream); |
| EXPECT_EQ(KURL("ws://example.com/chat"), stream->url()); |
| |
| ScriptPromiseTester connection_tester(script_state, |
| stream->connection(script_state)); |
| |
| stream->DidConnect("chat", "permessage-deflate"); |
| |
| connection_tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(connection_tester.IsFulfilled()); |
| v8::Local<v8::Value> value = connection_tester.Value().V8Value(); |
| ASSERT_FALSE(value.IsEmpty()); |
| ASSERT_TRUE(value->IsObject()); |
| EXPECT_EQ(PropertyAsString(script_state, value, "readable"), |
| "[object ReadableStream]"); |
| EXPECT_EQ(PropertyAsString(script_state, value, "writable"), |
| "[object WritableStream]"); |
| EXPECT_EQ(PropertyAsString(script_state, value, "protocol"), "chat"); |
| EXPECT_EQ(PropertyAsString(script_state, value, "extensions"), |
| "permessage-deflate"); |
| } |
| |
| TEST_F(WebSocketStreamTest, ConnectThenCloseCleanly) { |
| V8TestingScope scope; |
| |
| { |
| InSequence s; |
| EXPECT_CALL(Channel(), ApplyBackpressure()); |
| EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/echo"), String())) |
| .WillOnce(Return(true)); |
| EXPECT_CALL(Channel(), Close(-1, String(""))); |
| EXPECT_CALL(Channel(), Disconnect()); |
| } |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* stream = |
| Create(script_state, "ws://example.com/echo", ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(stream); |
| |
| stream->DidConnect("", ""); |
| |
| ScriptPromiseTester closed_tester(script_state, stream->closed(script_state)); |
| |
| stream->close(MakeGarbageCollected<WebSocketCloseInfo>(), |
| scope.GetExceptionState()); |
| stream->DidClose(WebSocketChannelClient::kClosingHandshakeComplete, 1005, ""); |
| |
| closed_tester.WaitUntilSettled(); |
| EXPECT_TRUE(closed_tester.IsFulfilled()); |
| ASSERT_TRUE(closed_tester.Value().IsObject()); |
| EXPECT_EQ( |
| PropertyAsString(script_state, closed_tester.Value().V8Value(), "code"), |
| "1005"); |
| EXPECT_EQ( |
| PropertyAsString(script_state, closed_tester.Value().V8Value(), "reason"), |
| ""); |
| } |
| |
| TEST_F(WebSocketStreamTest, CloseDuringHandshake) { |
| V8TestingScope scope; |
| |
| { |
| InSequence s; |
| EXPECT_CALL(Channel(), ApplyBackpressure()); |
| EXPECT_CALL(Channel(), Connect(KURL("ws://example.com/echo"), String())) |
| .WillOnce(Return(true)); |
| EXPECT_CALL( |
| Channel(), |
| FailMock( |
| String("WebSocket is closed before the connection is established."), |
| mojom::ConsoleMessageLevel::kWarning, _)); |
| EXPECT_CALL(Channel(), Disconnect()); |
| } |
| |
| auto* script_state = scope.GetScriptState(); |
| auto* stream = Create(scope.GetScriptState(), "ws://example.com/echo", |
| ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(stream); |
| |
| ScriptPromiseTester connection_tester(script_state, |
| stream->connection(script_state)); |
| ScriptPromiseTester closed_tester(script_state, stream->closed(script_state)); |
| |
| stream->close(MakeGarbageCollected<WebSocketCloseInfo>(), |
| scope.GetExceptionState()); |
| stream->DidClose(WebSocketChannelClient::kClosingHandshakeIncomplete, 1006, |
| ""); |
| |
| connection_tester.WaitUntilSettled(); |
| closed_tester.WaitUntilSettled(); |
| |
| EXPECT_TRUE(connection_tester.IsRejected()); |
| EXPECT_TRUE(IsDOMException(script_state, connection_tester.Value(), |
| DOMExceptionCode::kNetworkError)); |
| EXPECT_TRUE(closed_tester.IsRejected()); |
| EXPECT_TRUE(IsDOMException(script_state, closed_tester.Value(), |
| DOMExceptionCode::kNetworkError)); |
| } |
| |
| } // namespace |
| |
| } // namespace blink |