blob: 51b9aa891ec567761144941743102544f027e637 [file] [log] [blame]
// 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