/* * libjingle * Copyright 2004--2011, Google Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "talk/base/gunit.h" #include "talk/base/httpbase.h" #include "talk/base/testutils.h" namespace talk_base { const char* const kHttpResponse = "HTTP/1.1 200\r\n" "Connection: Keep-Alive\r\n" "Content-Type: text/plain\r\n" "Proxy-Authorization: 42\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "00000008\r\n" "Goodbye!\r\n" "0\r\n\r\n"; const char* const kHttpEmptyResponse = "HTTP/1.1 200\r\n" "Connection: Keep-Alive\r\n" "Content-Length: 0\r\n" "Proxy-Authorization: 42\r\n" "\r\n"; const char* const kHttpResponsePrefix = "HTTP/1.1 200\r\n" "Connection: Keep-Alive\r\n" "Content-Type: text/plain\r\n" "Proxy-Authorization: 42\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "8\r\n" "Goodbye!\r\n"; class HttpBaseTest : public testing::Test, public IHttpNotify { public: enum EventType { E_HEADER_COMPLETE, E_COMPLETE, E_CLOSED }; struct Event { EventType event; bool chunked; size_t data_size; HttpMode mode; HttpError err; }; HttpBaseTest() : mem(NULL), obtain_stream(false), http_stream(NULL) { } virtual void SetUp() { } virtual void TearDown() { delete http_stream; // Avoid an ASSERT, in case a test doesn't clean up properly base.abort(HE_NONE); } virtual HttpError onHttpHeaderComplete(bool chunked, size_t& data_size) { LOG_F(LS_VERBOSE) << "chunked: " << chunked << " size: " << data_size; Event e = { E_HEADER_COMPLETE, chunked, data_size, HM_NONE, HE_NONE}; events.push_back(e); if (obtain_stream) { ObtainDocumentStream(); } return HE_NONE; } virtual void onHttpComplete(HttpMode mode, HttpError err) { LOG_F(LS_VERBOSE) << "mode: " << mode << " err: " << err; Event e = { E_COMPLETE, false, 0, mode, err }; events.push_back(e); } virtual void onHttpClosed(HttpError err) { LOG_F(LS_VERBOSE) << "err: " << err; Event e = { E_CLOSED, false, 0, HM_NONE, err }; events.push_back(e); } void SetupSource(const char* response); void VerifyHeaderComplete(size_t event_count, bool empty_doc); void VerifyDocumentContents(const char* expected_data, size_t expected_length = SIZE_UNKNOWN); void ObtainDocumentStream(); void VerifyDocumentStreamIsOpening(); void VerifyDocumentStreamOpenEvent(); void ReadDocumentStreamData(const char* expected_data); void VerifyDocumentStreamIsEOS(); void SetupDocument(const char* response); void VerifySourceContents(const char* expected_data, size_t expected_length = SIZE_UNKNOWN); void VerifyTransferComplete(HttpMode mode, HttpError error); HttpBase base; MemoryStream* mem; HttpResponseData data; // The source of http data, and source events testing::StreamSource src; std::vector events; // Document stream, and stream events bool obtain_stream; StreamInterface* http_stream; testing::StreamSink sink; }; void HttpBaseTest::SetupSource(const char* http_data) { LOG_F(LS_VERBOSE) << "Enter"; src.SetState(SS_OPENING); src.QueueString(http_data); base.notify(this); base.attach(&src); EXPECT_TRUE(events.empty()); src.SetState(SS_OPEN); ASSERT_EQ(1U, events.size()); EXPECT_EQ(E_COMPLETE, events[0].event); EXPECT_EQ(HM_CONNECT, events[0].mode); EXPECT_EQ(HE_NONE, events[0].err); events.clear(); mem = new MemoryStream; data.document.reset(mem); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::VerifyHeaderComplete(size_t event_count, bool empty_doc) { LOG_F(LS_VERBOSE) << "Enter"; ASSERT_EQ(event_count, events.size()); EXPECT_EQ(E_HEADER_COMPLETE, events[0].event); std::string header; EXPECT_EQ(HVER_1_1, data.version); EXPECT_EQ(static_cast(HC_OK), data.scode); EXPECT_TRUE(data.hasHeader(HH_PROXY_AUTHORIZATION, &header)); EXPECT_EQ("42", header); EXPECT_TRUE(data.hasHeader(HH_CONNECTION, &header)); EXPECT_EQ("Keep-Alive", header); if (empty_doc) { EXPECT_FALSE(events[0].chunked); EXPECT_EQ(0U, events[0].data_size); EXPECT_TRUE(data.hasHeader(HH_CONTENT_LENGTH, &header)); EXPECT_EQ("0", header); } else { EXPECT_TRUE(events[0].chunked); EXPECT_EQ(SIZE_UNKNOWN, events[0].data_size); EXPECT_TRUE(data.hasHeader(HH_CONTENT_TYPE, &header)); EXPECT_EQ("text/plain", header); EXPECT_TRUE(data.hasHeader(HH_TRANSFER_ENCODING, &header)); EXPECT_EQ("chunked", header); } LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::VerifyDocumentContents(const char* expected_data, size_t expected_length) { LOG_F(LS_VERBOSE) << "Enter"; if (SIZE_UNKNOWN == expected_length) { expected_length = strlen(expected_data); } EXPECT_EQ(mem, data.document.get()); size_t length; mem->GetSize(&length); EXPECT_EQ(expected_length, length); EXPECT_TRUE(0 == memcmp(expected_data, mem->GetBuffer(), length)); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::ObtainDocumentStream() { LOG_F(LS_VERBOSE) << "Enter"; EXPECT_FALSE(http_stream); http_stream = base.GetDocumentStream(); ASSERT_TRUE(NULL != http_stream); sink.Monitor(http_stream); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::VerifyDocumentStreamIsOpening() { LOG_F(LS_VERBOSE) << "Enter"; ASSERT_TRUE(NULL != http_stream); EXPECT_EQ(0, sink.Events(http_stream)); EXPECT_EQ(SS_OPENING, http_stream->GetState()); size_t read = 0; char buffer[5] = { 0 }; EXPECT_EQ(SR_BLOCK, http_stream->Read(buffer, sizeof(buffer), &read, NULL)); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::VerifyDocumentStreamOpenEvent() { LOG_F(LS_VERBOSE) << "Enter"; ASSERT_TRUE(NULL != http_stream); EXPECT_EQ(SE_OPEN | SE_READ, sink.Events(http_stream)); EXPECT_EQ(SS_OPEN, http_stream->GetState()); // HTTP headers haven't arrived yet EXPECT_EQ(0U, events.size()); EXPECT_EQ(static_cast(HC_INTERNAL_SERVER_ERROR), data.scode); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::ReadDocumentStreamData(const char* expected_data) { LOG_F(LS_VERBOSE) << "Enter"; ASSERT_TRUE(NULL != http_stream); EXPECT_EQ(SS_OPEN, http_stream->GetState()); // Pump the HTTP I/O using Read, and verify the results. size_t verified_length = 0; const size_t expected_length = strlen(expected_data); while (verified_length < expected_length) { size_t read = 0; char buffer[5] = { 0 }; size_t amt_to_read = _min(expected_length - verified_length, sizeof(buffer)); EXPECT_EQ(SR_SUCCESS, http_stream->Read(buffer, amt_to_read, &read, NULL)); EXPECT_EQ(amt_to_read, read); EXPECT_TRUE(0 == memcmp(expected_data + verified_length, buffer, read)); verified_length += read; } LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::VerifyDocumentStreamIsEOS() { LOG_F(LS_VERBOSE) << "Enter"; ASSERT_TRUE(NULL != http_stream); size_t read = 0; char buffer[5] = { 0 }; EXPECT_EQ(SR_EOS, http_stream->Read(buffer, sizeof(buffer), &read, NULL)); EXPECT_EQ(SS_CLOSED, http_stream->GetState()); // When EOS is caused by Read, we don't expect SE_CLOSE EXPECT_EQ(0, sink.Events(http_stream)); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::SetupDocument(const char* document_data) { LOG_F(LS_VERBOSE) << "Enter"; src.SetState(SS_OPEN); base.notify(this); base.attach(&src); EXPECT_TRUE(events.empty()); if (document_data) { // Note: we could just call data.set_success("text/plain", mem), but that // won't allow us to use the chunked transfer encoding. mem = new MemoryStream(document_data); data.document.reset(mem); data.setHeader(HH_CONTENT_TYPE, "text/plain"); data.setHeader(HH_TRANSFER_ENCODING, "chunked"); } else { data.setHeader(HH_CONTENT_LENGTH, "0"); } data.scode = HC_OK; data.setHeader(HH_PROXY_AUTHORIZATION, "42"); data.setHeader(HH_CONNECTION, "Keep-Alive"); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::VerifySourceContents(const char* expected_data, size_t expected_length) { LOG_F(LS_VERBOSE) << "Enter"; if (SIZE_UNKNOWN == expected_length) { expected_length = strlen(expected_data); } std::string contents = src.ReadData(); EXPECT_EQ(expected_length, contents.length()); EXPECT_TRUE(0 == memcmp(expected_data, contents.data(), expected_length)); LOG_F(LS_VERBOSE) << "Exit"; } void HttpBaseTest::VerifyTransferComplete(HttpMode mode, HttpError error) { LOG_F(LS_VERBOSE) << "Enter"; // Verify that http operation has completed ASSERT_TRUE(events.size() > 0); size_t last_event = events.size() - 1; EXPECT_EQ(E_COMPLETE, events[last_event].event); EXPECT_EQ(mode, events[last_event].mode); EXPECT_EQ(error, events[last_event].err); LOG_F(LS_VERBOSE) << "Exit"; } // // Tests // TEST_F(HttpBaseTest, SupportsSend) { // Queue response document SetupDocument("Goodbye!"); // Begin send base.send(&data); // Send completed successfully VerifyTransferComplete(HM_SEND, HE_NONE); VerifySourceContents(kHttpResponse); } TEST_F(HttpBaseTest, SupportsSendNoDocument) { // Queue response document SetupDocument(NULL); // Begin send base.send(&data); // Send completed successfully VerifyTransferComplete(HM_SEND, HE_NONE); VerifySourceContents(kHttpEmptyResponse); } TEST_F(HttpBaseTest, SignalsCompleteOnInterruptedSend) { // This test is attempting to expose a bug that occurs when a particular // base objects is used for receiving, and then used for sending. In // particular, the HttpParser state is different after receiving. Simulate // that here. SetupSource(kHttpResponse); base.recv(&data); VerifyTransferComplete(HM_RECV, HE_NONE); src.Clear(); data.clear(true); events.clear(); base.detach(); // Queue response document SetupDocument("Goodbye!"); // Prevent entire response from being sent const size_t kInterruptedLength = strlen(kHttpResponse) - 1; src.SetWriteBlock(kInterruptedLength); // Begin send base.send(&data); // Document is mostly complete, but no completion signal yet. EXPECT_TRUE(events.empty()); VerifySourceContents(kHttpResponse, kInterruptedLength); src.SetState(SS_CLOSED); // Send completed with disconnect error, and no additional data. VerifyTransferComplete(HM_SEND, HE_DISCONNECTED); EXPECT_TRUE(src.ReadData().empty()); } TEST_F(HttpBaseTest, SupportsReceiveViaDocumentPush) { // Queue response document SetupSource(kHttpResponse); // Begin receive base.recv(&data); // Document completed successfully VerifyHeaderComplete(2, false); VerifyTransferComplete(HM_RECV, HE_NONE); VerifyDocumentContents("Goodbye!"); } TEST_F(HttpBaseTest, SupportsReceiveViaStreamPull) { // Switch to pull mode ObtainDocumentStream(); VerifyDocumentStreamIsOpening(); // Queue response document SetupSource(kHttpResponse); VerifyDocumentStreamIsOpening(); // Begin receive base.recv(&data); // Pull document data VerifyDocumentStreamOpenEvent(); ReadDocumentStreamData("Goodbye!"); VerifyDocumentStreamIsEOS(); // Document completed successfully VerifyHeaderComplete(2, false); VerifyTransferComplete(HM_RECV, HE_NONE); VerifyDocumentContents(""); } TEST_F(HttpBaseTest, DISABLED_AllowsCloseStreamBeforeDocumentIsComplete) { // TODO: Remove extra logging once test failure is understood int old_sev = talk_base::LogMessage::GetLogToDebug(); talk_base::LogMessage::LogToDebug(LS_VERBOSE); // Switch to pull mode ObtainDocumentStream(); VerifyDocumentStreamIsOpening(); // Queue response document SetupSource(kHttpResponse); VerifyDocumentStreamIsOpening(); // Begin receive base.recv(&data); // Pull some of the data VerifyDocumentStreamOpenEvent(); ReadDocumentStreamData("Goodb"); // We've seen the header by now VerifyHeaderComplete(1, false); // Close the pull stream, this will transition back to push I/O. http_stream->Close(); Thread::Current()->ProcessMessages(0); // Remainder of document completed successfully VerifyTransferComplete(HM_RECV, HE_NONE); VerifyDocumentContents("ye!"); talk_base::LogMessage::LogToDebug(old_sev); } TEST_F(HttpBaseTest, AllowsGetDocumentStreamInResponseToHttpHeader) { // Queue response document SetupSource(kHttpResponse); // Switch to pull mode in response to header arrival obtain_stream = true; // Begin receive base.recv(&data); // We've already seen the header, but not data has arrived VerifyHeaderComplete(1, false); VerifyDocumentContents(""); // Pull the document data ReadDocumentStreamData("Goodbye!"); VerifyDocumentStreamIsEOS(); // Document completed successfully VerifyTransferComplete(HM_RECV, HE_NONE); VerifyDocumentContents(""); } TEST_F(HttpBaseTest, AllowsGetDocumentStreamWithEmptyDocumentBody) { // Queue empty response document SetupSource(kHttpEmptyResponse); // Switch to pull mode in response to header arrival obtain_stream = true; // Begin receive base.recv(&data); // We've already seen the header, but not data has arrived VerifyHeaderComplete(1, true); VerifyDocumentContents(""); // The document is still open, until we attempt to read ASSERT_TRUE(NULL != http_stream); EXPECT_EQ(SS_OPEN, http_stream->GetState()); // Attempt to read data, and discover EOS VerifyDocumentStreamIsEOS(); // Document completed successfully VerifyTransferComplete(HM_RECV, HE_NONE); VerifyDocumentContents(""); } TEST_F(HttpBaseTest, SignalsDocumentStreamCloseOnUnexpectedClose) { // Switch to pull mode ObtainDocumentStream(); VerifyDocumentStreamIsOpening(); // Queue response document SetupSource(kHttpResponsePrefix); VerifyDocumentStreamIsOpening(); // Begin receive base.recv(&data); // Pull document data VerifyDocumentStreamOpenEvent(); ReadDocumentStreamData("Goodbye!"); // Simulate unexpected close src.SetState(SS_CLOSED); // Observe error event on document stream EXPECT_EQ(testing::SSE_ERROR, sink.Events(http_stream)); // Future reads give an error int error = 0; char buffer[5] = { 0 }; EXPECT_EQ(SR_ERROR, http_stream->Read(buffer, sizeof(buffer), NULL, &error)); EXPECT_EQ(HE_DISCONNECTED, error); // Document completed with error VerifyHeaderComplete(2, false); VerifyTransferComplete(HM_RECV, HE_DISCONNECTED); VerifyDocumentContents(""); } } // namespace talk_base