blob: bc90b618c6b9623e3494ad83957dfea5289f9bff [file] [log] [blame]
// Copyright 2015 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: jefftk@google.com (Jeff Kaufman)
#include "pagespeed/apache/apache_fetch.h"
#include "pagespeed/apache/header_util.h"
#include "pagespeed/apache/mock_apache.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/null_mutex.h"
#include "pagespeed/kernel/base/null_thread_system.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "httpd.h" // NOLINT
namespace net_instaweb {
class TimedWaitCallbackCallDone : public NullCondvar::TimedWaitCallback {
public:
explicit TimedWaitCallbackCallDone(ApacheFetch* apache_fetch)
: apache_fetch_(apache_fetch) {}
virtual void Call() { apache_fetch_->Done(true /* success */); }
private:
ApacheFetch* apache_fetch_;
};
// The standard way to use an ApacheFetch is with two threads:
// apache thread:
// - set things up
// - put a rewrite task on the rewrite thread
// - wait (timed)
// - success: done, can use the fetch
// - timeout: abandon fetch to complete in the background, can't use it
// rewrite thread:
// - call Write(), Flush(), etc
// - send things through to Apache if not abandoned
// - call Done()
// - if abandoned we delete ourself
// - otherwise signal the apache thread to stop waiting
//
// To test this flow here we run both sides in the same thread. Success and
// timeout are mocked differently:
//
// success:
// - Use a NullCondvar so TimedWait will immediately invoke Done() on the
// ApacheFetch. Now Wait() returns via the success path.
// timeout:
// - Set the timeout to -1 and make TimedWait return immediately. Now Wait()
// returns via the abandonment path.
//
class ApacheFetchTest : public testing::Test {
public:
ApacheFetchTest()
: mapped_url_("http://www.example.com"),
debug_info_("ignored"),
thread_system_(),
timer_(new NullMutex(), 0),
apache_writer_(new ApacheWriter(&request_, &thread_system_)),
request_headers_(new RequestHeaders()),
request_ctx_(RequestContext::NewTestRequestContextWithTimer(
&thread_system_, &timer_)),
message_handler_(new NullMutex()) {
MockApache::Initialize();
MockApache::PrepareRequest(&request_);
RewriteOptions::Initialize();
rewrite_options_.reset(new RewriteOptions(&thread_system_));
// Takes ownership of apache_writer and request_headers but nothing else.
// Keeping a copy them violates ApacheFetch's mutex guarantees, and should
// only be done by tests.
apache_fetch_.reset(new ApacheFetch(
mapped_url_, debug_info_, &thread_system_, &timer_,
apache_writer_, request_headers_, request_ctx_,
rewrite_options_.get(), &message_handler_));
condvar_ = dynamic_cast<NullCondvar*>(apache_fetch_->CondvarForTesting());
CHECK(condvar_ != NULL);
// These can be overriden later in tests that want to test something
// unusual.
apache_fetch_->response_headers()->set_status_code(200 /* OK */);
// HTTP 1.1 is most common now.
apache_fetch_->response_headers()->set_major_version(1);
apache_fetch_->response_headers()->set_minor_version(1);
// Responses need a content type or we will 403.
apache_fetch_->response_headers()->MergeContentType("text/plain");
// Set cookies to make sure they get removed.
apache_fetch_->response_headers()->Add("Set-Cookie", "test=cookie");
apache_fetch_->response_headers()->Add("Set-Cookie", "tasty=cookie");
apache_fetch_->response_headers()->Add("Set-Cookie2", "obselete");
apache_fetch_->set_max_wait_ms(-1); // Never actually wait in tests.
// None of these should have been passed through to the apache_writer.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
}
virtual ~ApacheFetchTest() {
RewriteOptions::Terminate();
MockApache::CleanupRequest(&request_);
MockApache::Terminate();
}
protected:
// After a request is all set up, run through a successful completion and
// verify that no issues were sent to the message handler.
void WaitExpectSuccess() {
TimedWaitCallbackCallDone callback(apache_fetch_.get());
condvar_->set_timed_wait_callback(&callback);
EXPECT_EQ(true, apache_fetch_->Wait(NULL /* rewrite_driver */));
EXPECT_EQ(0, message_handler_.TotalMessages());
EXPECT_EQ("TimedWait(5000) Signal()", condvar_->ActionsSinceLastCall());
}
GoogleString SynthesizeWarning(StringPiece message) {
return StrCat("W[Wed Jan 01 00:00:00 2014] [Warning] [00000] ", message,
"\n");
}
// After a request is set up and Wait returned that the fetch had been
// abandoned, finish the fetch and verify that we get the error messages we
// expected.
void PostAbandonmentHelper() {
// Should normally null apache_fetch_ here because Wait failed but we're
// mocking multiple threads here and still have to call Done() on it.
EXPECT_FALSE(apache_fetch_->Write("abandoned", &message_handler_));
// Write dropped.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
EXPECT_EQ("TimedWait(5000)", // Note no Signal().
condvar_->ActionsSinceLastCall());
// Simulate the fetch finishing after being abandoned.
apache_fetch_->Done(true /* success */);
apache_fetch_.release(); // An abandoned fetch deletes itself on Done().
GoogleString message_output;
StringWriter writer(&message_output);
message_handler_.Dump(&writer);
GoogleString expected = StrCat(
SynthesizeWarning(
"Abandoned URL http://www.example.com after 0 sec "
"(debug=ignored,"
" driver=null rewrite driver, should only be used in tests)."),
SynthesizeWarning(
"Write of 9 bytes for url http://www.example.com received "
"after being abandoned for timing out."),
SynthesizeWarning(
"Response for url http://www.example.com completed "
"with status 200 (null) after being abandoned for timing out."));
EXPECT_EQ(expected, message_output);
}
GoogleString mapped_url_;
GoogleString debug_info_;
NullThreadSystem thread_system_;
MockTimer timer_;
request_rec request_;
ApacheWriter* apache_writer_;
RequestHeaders* request_headers_;
RequestContextPtr request_ctx_;
MockMessageHandler message_handler_;
scoped_ptr<RewriteOptions> rewrite_options_;
scoped_ptr<ApacheFetch> apache_fetch_;
NullCondvar* condvar_; // owned by apache_fetch_
};
TEST_F(ApacheFetchTest, SuccessBuffered) {
EXPECT_TRUE(apache_fetch_->Write("hello ", &message_handler_));
EXPECT_TRUE(apache_fetch_->Write("world", &message_handler_));
EXPECT_TRUE(apache_fetch_->Flush(&message_handler_));
EXPECT_TRUE(apache_fetch_->Write(".", &message_handler_));
EXPECT_TRUE(apache_fetch_->Flush(&message_handler_));
// All flushes are dropped, all writed are buffered.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
WaitExpectSuccess();
EXPECT_EQ(200, request_.status);
EXPECT_EQ(
"ap_set_content_type(text/plain) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/plain) "
"ap_rwrite(hello world.)", /* note: writes combined into one call */
MockApache::ActionsSinceLastCall());
// TODO(jefftk): Cookies are present here even though we asked ApacheWriter to
// remove them because of an ApacheWriter bug.
EXPECT_EQ("Content-Type: text/plain\n"
"Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
}
TEST_F(ApacheFetchTest, SuccessUnbuffered) {
apache_fetch_->set_buffered(false);
EXPECT_TRUE(apache_fetch_->Write("hello ", &message_handler_));
EXPECT_EQ(200, request_.status);
EXPECT_EQ(
"ap_set_content_type(text/plain) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/plain) "
"ap_rwrite(hello )",
MockApache::ActionsSinceLastCall());
// TODO(jefftk): Cookies are present here even though we asked ApacheWriter to
// remove them because of an ApacheWriter bug.
EXPECT_EQ("Content-Type: text/plain\n"
"Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_TRUE(apache_fetch_->Write("world", &message_handler_));
EXPECT_EQ("ap_rwrite(world)", MockApache::ActionsSinceLastCall());
EXPECT_TRUE(apache_fetch_->Flush(&message_handler_));
EXPECT_EQ("ap_rflush()", MockApache::ActionsSinceLastCall());
EXPECT_TRUE(apache_fetch_->Write(".", &message_handler_));
EXPECT_EQ("ap_rwrite(.)", MockApache::ActionsSinceLastCall());
EXPECT_TRUE(apache_fetch_->Flush(&message_handler_));
EXPECT_EQ("ap_rflush()", MockApache::ActionsSinceLastCall());
apache_fetch_->Done(true);
}
TEST_F(ApacheFetchTest, NotFound404Buffered) {
apache_fetch_->response_headers()->set_status_code(404);
EXPECT_TRUE(apache_fetch_->Write("Couldn't find it.", &message_handler_));
// Writes are buffered.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
WaitExpectSuccess();
EXPECT_EQ(404, request_.status);
EXPECT_EQ("Content-Type: text/plain\n"
"Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_set_content_type(text/plain) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/plain) "
"ap_rwrite(Couldn't find it.)",
MockApache::ActionsSinceLastCall());
}
TEST_F(ApacheFetchTest, NotFound404Unbuffered) {
apache_fetch_->set_buffered(false);
apache_fetch_->response_headers()->set_status_code(404);
EXPECT_TRUE(apache_fetch_->Write("Couldn't find it.", &message_handler_));
EXPECT_EQ(404, request_.status);
EXPECT_EQ("Content-Type: text/plain\n"
"Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_set_content_type(text/plain) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/plain) "
"ap_rwrite(Couldn't find it.)",
MockApache::ActionsSinceLastCall());
apache_fetch_->Done(true);
}
TEST_F(ApacheFetchTest, NoContentType200Buffered) {
apache_fetch_->response_headers()->set_status_code(200);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll(
HttpAttributes::kContentType));
EXPECT_TRUE(apache_fetch_->Write("Example response.", &message_handler_));
// Writes are buffered.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
WaitExpectSuccess();
EXPECT_EQ(403, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Content-Type: text/html\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_set_content_type(text/html) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/html) "
"ap_rwrite(Missing Content-Type required for proxied resource)",
MockApache::ActionsSinceLastCall());
}
TEST_F(ApacheFetchTest, NoContentType200Unbuffered) {
apache_fetch_->set_buffered(false);
apache_fetch_->response_headers()->set_status_code(200);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll(
HttpAttributes::kContentType));
EXPECT_TRUE(apache_fetch_->Write("Example response.", &message_handler_));
EXPECT_EQ(403, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Content-Type: text/html\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_set_content_type(text/html) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/html) "
"ap_rwrite(Missing Content-Type required for proxied resource)",
MockApache::ActionsSinceLastCall());
apache_fetch_->Done(true);
}
TEST_F(ApacheFetchTest, NoContentType301Buffered) {
apache_fetch_->response_headers()->set_status_code(301);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type"));
apache_fetch_->response_headers()->Add("Location", "elsewhere");
EXPECT_TRUE(apache_fetch_->Write("moved elsewhere", &message_handler_));
// Writes are buffered.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
WaitExpectSuccess();
EXPECT_EQ(403, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Location: elsewhere\n"
"Content-Type: text/html\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_set_content_type(text/html) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/html) "
"ap_rwrite(Missing Content-Type required for proxied resource)",
MockApache::ActionsSinceLastCall());
}
TEST_F(ApacheFetchTest, NoContentType301Unbuffered) {
apache_fetch_->set_buffered(false);
apache_fetch_->response_headers()->set_status_code(301);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type"));
apache_fetch_->response_headers()->Add("Location", "elsewhere");
EXPECT_TRUE(apache_fetch_->Write("moved elsewhere", &message_handler_));
EXPECT_EQ(403, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Location: elsewhere\n"
"Content-Type: text/html\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_set_content_type(text/html) "
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_set_content_type(text/html) "
"ap_rwrite(Missing Content-Type required for proxied resource)",
MockApache::ActionsSinceLastCall());
apache_fetch_->Done(true);
}
TEST_F(ApacheFetchTest, NoContentType304Buffered) {
apache_fetch_->response_headers()->set_status_code(304);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type"));
EXPECT_TRUE(apache_fetch_->Write("not modified", &message_handler_));
// Writes are buffered.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
WaitExpectSuccess();
EXPECT_EQ(304, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_rwrite(not modified)",
MockApache::ActionsSinceLastCall());
}
TEST_F(ApacheFetchTest, NoContentType304Unbuffered) {
apache_fetch_->set_buffered(false);
apache_fetch_->response_headers()->set_status_code(304);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type"));
EXPECT_TRUE(apache_fetch_->Write("not modified", &message_handler_));
EXPECT_EQ(304, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ(
"ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT) "
"ap_rwrite(not modified)",
MockApache::ActionsSinceLastCall());
apache_fetch_->Done(true);
}
TEST_F(ApacheFetchTest, NoContentType204Buffered) {
apache_fetch_->response_headers()->set_status_code(204);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type"));
apache_fetch_->HeadersComplete();
// Headers are buffered.
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
WaitExpectSuccess();
EXPECT_EQ(204, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ("ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT)",
MockApache::ActionsSinceLastCall());
}
TEST_F(ApacheFetchTest, NoContentType204Unbuffered) {
apache_fetch_->set_buffered(false);
apache_fetch_->response_headers()->set_status_code(204);
EXPECT_TRUE(apache_fetch_->response_headers()->RemoveAll("Content-Type"));
apache_fetch_->HeadersComplete();
EXPECT_EQ(204, request_.status);
EXPECT_EQ("Set-Cookie: test=cookie\n"
"Set-Cookie: tasty=cookie\n"
"Set-Cookie2: obselete\n"
"Date: Thu, 01 Jan 1970 00:00:00 GMT\n"
"X-Content-Type-Options: nosniff\n"
"Cache-Control: max-age=0, no-cache\n",
HeadersOutToString(&request_));
EXPECT_EQ("ap_remove_output_filter(MOD_EXPIRES) "
"ap_remove_output_filter(FIXUP_HEADERS_OUT)",
MockApache::ActionsSinceLastCall());
apache_fetch_->Done(true);
}
TEST_F(ApacheFetchTest, AbandonedAfterWritingBuffered) {
// Write() will call HeadersComplete().
EXPECT_TRUE(apache_fetch_->Write("hello ", &message_handler_));
EXPECT_EQ("", MockApache::ActionsSinceLastCall());
EXPECT_FALSE(apache_fetch_->Wait(NULL /* rewrite_driver */));
PostAbandonmentHelper();
}
TEST_F(ApacheFetchTest, AbandonedBeforeWritingBuffered) {
EXPECT_FALSE(apache_fetch_->Wait(NULL /* rewrite_driver */));
PostAbandonmentHelper();
}
// Unbuffered fetches can't get abandoned, so not testing that case.
} // namespace net_instaweb