Chromium Code Reviews| Index: net/websockets/websocket_stream_test.cc |
| diff --git a/net/websockets/websocket_stream_test.cc b/net/websockets/websocket_stream_test.cc |
| index 0a6b99be4d60f68404d97f5cda56d7af7f20ee8b..faf06476a80e7c1db939a714bd883d3d7b57956f 100644 |
| --- a/net/websockets/websocket_stream_test.cc |
| +++ b/net/websockets/websocket_stream_test.cc |
| @@ -55,6 +55,29 @@ std::vector<HeaderKeyValuePair> ToVector(const HttpResponseHeaders& headers) { |
| return result; |
| } |
| +// Simple builder for a DeterministicSocketData object to save repetitive code. |
| +// It always sets the connect data to MockConnect(SYNCHRONOUS, OK), so it cannot |
| +// be used in tests where the connect fails. In practice, those tests never have |
| +// any read/write data and so can't benefit from it anyway. The arrays are not |
| +// copied. It is up to the caller to ensure they stay in scope until the test |
| +// ends. |
| +template <size_t reads_count, size_t writes_count> |
| +scoped_ptr<DeterministicSocketData> BuildSocketData( |
| + MockRead (&reads)[reads_count], |
| + MockWrite (&writes)[writes_count]) { |
| + scoped_ptr<DeterministicSocketData> socket_data( |
| + new DeterministicSocketData(reads, reads_count, writes, writes_count)); |
| + socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| + socket_data->SetStop(reads_count + writes_count); |
| + return socket_data.Pass(); |
| +} |
| + |
| +// Builder for a DeterministicSocketData that expects nothing. This does not |
| +// set the connect data, so the calling code must do that explicitly. |
| +scoped_ptr<DeterministicSocketData> BuildNullSocketData() { |
| + return make_scoped_ptr(new DeterministicSocketData(NULL, 0, NULL, 0)); |
| +} |
| + |
| // A sub-class of WebSocketHandshakeStreamCreateHelper which always sets a |
| // deterministic key to use in the WebSocket handshake. |
| class DeterministicKeyWebSocketHandshakeStreamCreateHelper |
| @@ -118,10 +141,15 @@ class WebSocketStreamCreateTest : public ::testing::Test { |
| const std::vector<std::string>& sub_protocols, |
| const std::string& origin, |
| scoped_ptr<DeterministicSocketData> socket_data) { |
| - url_request_context_host_.AddRawExpectations(socket_data.Pass()); |
| + AddRawExpectations(socket_data.Pass()); |
| CreateAndConnectStream(socket_url, sub_protocols, origin); |
| } |
| + // Add additional raw expectations for sockets created before the final one. |
| + void AddRawExpectations(scoped_ptr<DeterministicSocketData> socket_data) { |
| + url_request_context_host_.AddRawExpectations(socket_data.Pass()); |
| + } |
| + |
| // A wrapper for CreateAndConnectStreamForTesting that knows about our default |
| // parameters. |
| void CreateAndConnectStream(const std::string& socket_url, |
| @@ -174,8 +202,8 @@ class WebSocketStreamCreateTest : public ::testing::Test { |
| virtual void OnStartOpeningHandshake( |
| scoped_ptr<WebSocketHandshakeRequestInfo> request) OVERRIDE { |
| - if (owner_->request_info_) |
| - ADD_FAILURE(); |
| + // Can be called multiple times (in the case of HTTP auth). Last call |
| + // wins. |
| owner_->request_info_ = request.Pass(); |
| } |
| virtual void OnFinishOpeningHandshake( |
| @@ -233,6 +261,128 @@ class WebSocketStreamCreateExtensionTest : public WebSocketStreamCreateTest { |
| } |
| }; |
| +// Common code to construct expectations for authentication tests that receive |
| +// the auth challenge on one connection and then create a second connection to |
| +// send the authenticated request on. |
| +class CommonAuthTestHelper { |
| + public: |
| + scoped_ptr<DeterministicSocketData> BuildSocketData1( |
| + const std::string& response) { |
| + request1 = WebSocketStandardRequest("/", "http://localhost", ""); |
| + writes1[0] = MockWrite(SYNCHRONOUS, 0, request1.c_str()); |
| + response1 = response; |
| + reads1[0] = MockRead(SYNCHRONOUS, 1, response1.c_str()); |
| + reads1[1] = MockRead(SYNCHRONOUS, OK, 2); // Close connection |
| + |
| + return BuildSocketData(reads1, writes1); |
| + } |
| + |
| + scoped_ptr<DeterministicSocketData> BuildSocketData2( |
| + const std::string& request, |
| + const std::string& response) { |
| + request2 = request; |
| + response2 = response; |
| + writes2[0] = MockWrite(SYNCHRONOUS, 0, request2.c_str()); |
| + reads2[0] = MockRead(SYNCHRONOUS, 1, response2.c_str()); |
| + return BuildSocketData(reads2, writes2); |
| + } |
| + |
| + private: |
| + // These need to be object-scoped since they have to remain valid until all |
| + // socket operations in the test are complete. |
| + std::string request1; |
|
Johnny
2014/07/08 20:27:40
Trailing underscores?
Adam Rice
2014/07/09 06:35:01
Okay, that's weird. In my brain this was a struct,
|
| + std::string request2; |
| + std::string response1; |
| + std::string response2; |
| + MockRead reads1[2]; |
| + MockWrite writes1[1]; |
| + MockRead reads2[1]; |
| + MockWrite writes2[1]; |
| +}; |
| + |
| +// Data and methods for BasicAuth tests. |
| +class WebSocketStreamCreateBasicAuthTest : public WebSocketStreamCreateTest { |
| + protected: |
| + void CreateAndConnectAuthHandshake(const std::string& url, |
| + const std::string& base64_user_pass, |
| + const std::string& response2) { |
| + AddRawExpectations(helper_.BuildSocketData1(kUnauthorizedResponse)); |
| + |
| + static const char request2format[] = |
| + "GET / HTTP/1.1\r\n" |
| + "Host: localhost\r\n" |
| + "Connection: Upgrade\r\n" |
| + "Pragma: no-cache\r\n" |
| + "Cache-Control: no-cache\r\n" |
| + "Authorization: Basic %s\r\n" |
| + "Upgrade: websocket\r\n" |
| + "Origin: http://localhost\r\n" |
| + "Sec-WebSocket-Version: 13\r\n" |
| + "User-Agent:\r\n" |
| + "Accept-Encoding: gzip,deflate\r\n" |
| + "Accept-Language: en-us,fr\r\n" |
| + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" |
| + "Sec-WebSocket-Extensions: permessage-deflate; " |
| + "client_max_window_bits\r\n" |
| + "\r\n"; |
| + const std::string request = |
| + base::StringPrintf(request2format, base64_user_pass.c_str()); |
| + CreateAndConnectRawExpectations( |
| + url, |
| + NoSubProtocols(), |
| + "http://localhost", |
| + helper_.BuildSocketData2(request, response2)); |
| + } |
| + |
| + static const char kUnauthorizedResponse[]; |
| + |
| + CommonAuthTestHelper helper_; |
| +}; |
| + |
| +class WebSocketStreamCreateDigestAuthTest : public WebSocketStreamCreateTest { |
| + protected: |
| + static const char kUnauthorizedResponse[]; |
| + static const char kAuthorizedRequest[]; |
| + |
| + CommonAuthTestHelper helper_; |
| +}; |
| + |
| +const char WebSocketStreamCreateBasicAuthTest::kUnauthorizedResponse[] = |
| + "HTTP/1.1 401 Unauthorized\r\n" |
| + "Content-Length: 0\r\n" |
| + "WWW-Authenticate: Basic realm=\"camelot\"\r\n" |
| + "\r\n"; |
| + |
| +// These negotiation values are borrowed from |
| +// http_auth_handler_digest_unittest.cc. Feel free to come up with new ones if |
| +// you are bored. Only the weakest (no qop) variants of Digest authentication |
| +// can be tested by this method, because the others involve random input. |
| +const char WebSocketStreamCreateDigestAuthTest::kUnauthorizedResponse[] = |
| + "HTTP/1.1 401 Unauthorized\r\n" |
| + "Content-Length: 0\r\n" |
| + "WWW-Authenticate: Digest realm=\"Oblivion\", nonce=\"nonce-value\"\r\n" |
| + "\r\n"; |
| + |
| +const char WebSocketStreamCreateDigestAuthTest::kAuthorizedRequest[] = |
| + "GET / HTTP/1.1\r\n" |
| + "Host: localhost\r\n" |
| + "Connection: Upgrade\r\n" |
| + "Pragma: no-cache\r\n" |
| + "Cache-Control: no-cache\r\n" |
| + "Authorization: Digest username=\"FooBar\", realm=\"Oblivion\", " |
| + "nonce=\"nonce-value\", uri=\"/\", " |
| + "response=\"f72ff54ebde2f928860f806ec04acd1b\"\r\n" |
| + "Upgrade: websocket\r\n" |
| + "Origin: http://localhost\r\n" |
| + "Sec-WebSocket-Version: 13\r\n" |
| + "User-Agent:\r\n" |
| + "Accept-Encoding: gzip,deflate\r\n" |
| + "Accept-Language: en-us,fr\r\n" |
| + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" |
| + "Sec-WebSocket-Extensions: permessage-deflate; " |
| + "client_max_window_bits\r\n" |
| + "\r\n"; |
| + |
| class WebSocketStreamCreateUMATest : public ::testing::Test { |
| public: |
| // This enum should match with the enum in Delegate in websocket_stream.cc. |
| @@ -925,8 +1075,7 @@ TEST_F(WebSocketStreamCreateTest, Cancellation) { |
| // Connect failure must look just like negotiation failure. |
| TEST_F(WebSocketStreamCreateTest, ConnectionFailure) { |
| - scoped_ptr<DeterministicSocketData> socket_data( |
| - new DeterministicSocketData(NULL, 0, NULL, 0)); |
| + scoped_ptr<DeterministicSocketData> socket_data(BuildNullSocketData()); |
| socket_data->set_connect_data( |
| MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED)); |
| CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), |
| @@ -941,8 +1090,7 @@ TEST_F(WebSocketStreamCreateTest, ConnectionFailure) { |
| // Connect timeout must look just like any other failure. |
| TEST_F(WebSocketStreamCreateTest, ConnectionTimeout) { |
| - scoped_ptr<DeterministicSocketData> socket_data( |
| - new DeterministicSocketData(NULL, 0, NULL, 0)); |
| + scoped_ptr<DeterministicSocketData> socket_data(BuildNullSocketData()); |
| socket_data->set_connect_data( |
| MockConnect(ASYNC, ERR_CONNECTION_TIMED_OUT)); |
| CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), |
| @@ -955,8 +1103,7 @@ TEST_F(WebSocketStreamCreateTest, ConnectionTimeout) { |
| // Cancellation during connect works. |
| TEST_F(WebSocketStreamCreateTest, CancellationDuringConnect) { |
| - scoped_ptr<DeterministicSocketData> socket_data( |
| - new DeterministicSocketData(NULL, 0, NULL, 0)); |
| + scoped_ptr<DeterministicSocketData> socket_data(BuildNullSocketData()); |
| socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING)); |
| CreateAndConnectRawExpectations("ws://localhost/", |
| NoSubProtocols(), |
| @@ -998,15 +1145,15 @@ TEST_F(WebSocketStreamCreateTest, CancellationDuringRead) { |
| MockRead reads[] = { |
| MockRead(ASYNC, 1, "HTTP/1.1 101 Switching Protocols\r\nUpgr"), |
| }; |
| - DeterministicSocketData* socket_data(new DeterministicSocketData( |
| - reads, arraysize(reads), writes, arraysize(writes))); |
| - socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| + scoped_ptr<DeterministicSocketData> socket_data( |
| + BuildSocketData(reads, writes)); |
| socket_data->SetStop(1); |
| + DeterministicSocketData* socket_data_raw_ptr = socket_data.get(); |
| CreateAndConnectRawExpectations("ws://localhost/", |
| NoSubProtocols(), |
| "http://localhost", |
| - make_scoped_ptr(socket_data)); |
| - socket_data->Run(); |
| + socket_data.Pass()); |
|
Johnny
2014/07/08 20:27:40
Why is ownership of |socket_data| passed to WebSoc
Adam Rice
2014/07/09 06:35:01
Short explanation: WebSocketDeterministicMockClien
Johnny
2014/07/09 16:16:17
SGTM.
|
| + socket_data_raw_ptr->Run(); |
| stream_request_.reset(); |
| RunUntilIdle(); |
| EXPECT_FALSE(has_failed()); |
| @@ -1039,14 +1186,14 @@ TEST_F(WebSocketStreamCreateTest, NoResponse) { |
| std::string request = WebSocketStandardRequest("/", "http://localhost", ""); |
| MockWrite writes[] = {MockWrite(ASYNC, request.data(), request.size(), 0)}; |
| MockRead reads[] = {MockRead(ASYNC, 0, 1)}; |
| - DeterministicSocketData* socket_data(new DeterministicSocketData( |
| - reads, arraysize(reads), writes, arraysize(writes))); |
| - socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); |
| + scoped_ptr<DeterministicSocketData> socket_data( |
| + BuildSocketData(reads, writes)); |
| + DeterministicSocketData* socket_data_raw_ptr = socket_data.get(); |
| CreateAndConnectRawExpectations("ws://localhost/", |
| NoSubProtocols(), |
| "http://localhost", |
| - make_scoped_ptr(socket_data)); |
| - socket_data->RunFor(2); |
| + socket_data.Pass()); |
| + socket_data_raw_ptr->RunFor(2); |
| EXPECT_TRUE(has_failed()); |
| EXPECT_FALSE(stream_); |
| EXPECT_FALSE(response_info_); |
| @@ -1060,8 +1207,7 @@ TEST_F(WebSocketStreamCreateTest, SelfSignedCertificateFailure) { |
| ssl_data_[0]->cert = |
| ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der"); |
| ASSERT_TRUE(ssl_data_[0]->cert); |
| - scoped_ptr<DeterministicSocketData> raw_socket_data( |
| - new DeterministicSocketData(NULL, 0, NULL, 0)); |
| + scoped_ptr<DeterministicSocketData> raw_socket_data(BuildNullSocketData()); |
| CreateAndConnectRawExpectations("wss://localhost/", |
| NoSubProtocols(), |
| "http://localhost", |
| @@ -1084,8 +1230,7 @@ TEST_F(WebSocketStreamCreateTest, SelfSignedCertificateSuccess) { |
| ssl_data_.push_back(ssl_data.release()); |
| ssl_data.reset(new SSLSocketDataProvider(ASYNC, OK)); |
| ssl_data_.push_back(ssl_data.release()); |
| - url_request_context_host_.AddRawExpectations( |
| - make_scoped_ptr(new DeterministicSocketData(NULL, 0, NULL, 0))); |
| + url_request_context_host_.AddRawExpectations(BuildNullSocketData()); |
| CreateAndConnectStandard( |
| "wss://localhost/", "/", NoSubProtocols(), "http://localhost", "", ""); |
| RunUntilIdle(); |
| @@ -1096,6 +1241,60 @@ TEST_F(WebSocketStreamCreateTest, SelfSignedCertificateSuccess) { |
| EXPECT_TRUE(stream_); |
| } |
| +// If the server requests authorisation, but we have no credentials, the |
| +// connection should fail cleanly. |
| +TEST_F(WebSocketStreamCreateBasicAuthTest, FailureNoCredentials) { |
| + CreateAndConnectCustomResponse("ws://localhost/", |
| + "/", |
| + NoSubProtocols(), |
| + "http://localhost", |
| + "", |
| + kUnauthorizedResponse); |
| + RunUntilIdle(); |
| + EXPECT_TRUE(has_failed()); |
| + EXPECT_EQ("HTTP Authentication failed; no valid credentials available", |
| + failure_message()); |
| + EXPECT_TRUE(response_info_); |
| +} |
| + |
| +TEST_F(WebSocketStreamCreateBasicAuthTest, SuccessPasswordInUrl) { |
| + CreateAndConnectAuthHandshake("ws://foo:bar@localhost/", |
| + "Zm9vOmJhcg==", |
| + WebSocketStandardResponse(std::string())); |
| + RunUntilIdle(); |
| + EXPECT_FALSE(has_failed()); |
| + EXPECT_TRUE(stream_); |
| + ASSERT_TRUE(response_info_); |
| + EXPECT_EQ(101, response_info_->status_code); |
| +} |
| + |
| +TEST_F(WebSocketStreamCreateBasicAuthTest, FailureIncorrectPasswordInUrl) { |
| + CreateAndConnectAuthHandshake( |
| + "ws://foo:baz@localhost/", "Zm9vOmJheg==", kUnauthorizedResponse); |
| + RunUntilIdle(); |
| + EXPECT_TRUE(has_failed()); |
| + EXPECT_TRUE(response_info_); |
| +} |
| + |
| +// Digest auth has the same connection semantics as Basic auth, so we can |
| +// generally assume that whatever works for Basic auth will also work for |
| +// Digest. There's just one test here, to confirm that it works at all. |
| +TEST_F(WebSocketStreamCreateDigestAuthTest, DigestPasswordInUrl) { |
| + AddRawExpectations(helper_.BuildSocketData1(kUnauthorizedResponse)); |
| + |
| + CreateAndConnectRawExpectations( |
| + "ws://FooBar:pass@localhost/", |
| + NoSubProtocols(), |
| + "http://localhost", |
| + helper_.BuildSocketData2(kAuthorizedRequest, |
| + WebSocketStandardResponse(std::string()))); |
| + RunUntilIdle(); |
| + EXPECT_FALSE(has_failed()); |
| + EXPECT_TRUE(stream_); |
| + ASSERT_TRUE(response_info_); |
| + EXPECT_EQ(101, response_info_->status_code); |
| +} |
| + |
| TEST_F(WebSocketStreamCreateUMATest, Incomplete) { |
| const std::string name("Net.WebSocket.HandshakeResult"); |
| scoped_ptr<base::HistogramSamples> original(GetSamples(name)); |