package Test::Varnish; our $VERSION = '0.03'; use warnings; use strict; use Carp; use Getopt::Long; use HTTP::Cookies; use HTTP::Request; use LWP::UserAgent; use Test::More; use URI; sub analyze_response { my ($self, $res) = @_; my $cached = 0; if ($self->verbose) { my $hdr_obj = $res->headers; my @hdr_names = $hdr_obj->header_field_names; # Only "X-Varnish" is the standard, but some people use # custom and/or debugging headers for my $name (@hdr_names) { next unless $name =~ m{^X\-Varnish}; my $value = $res->header($name) || q{}; diag("$name: $value"); } } my $main_header = $res->header("X-Varnish"); #my $status = $res->header("X-Varnish-Status"); #my $cacheable = $res->header("X-Varnish-Cacheable"); # "X-Varnish: 2131920313 1299858343" means cached # "X-Varnish: 2039442137" means not cached if (defined $main_header && $main_header =~ m{^ \s* \d+ \s+ \d+ \s* $}mx) { $cached = 1; } return $cached; } sub new { my ($class, $opt) = @_; $class = ref $class || $class; $opt ||= {}; my $self = { _verbose => $opt->{verbose}, }; bless $self, $class; } sub is_cached { my ($self, $args, $message) = @_; my $is_cached = $self->_is_cached($args); my $url = $args->{url}; if (! defined $is_cached) { $message ||= qq{Request to url '$url' failed}; return ok(0 => $message); } $message ||= qq{Request to url '$url' should be cached by Varnish}; return ok($is_cached, $message); } sub isnt_cached { my ($self, $args, $message) = @_; my $is_cached = $self->_is_cached($args); my $url = $args->{url}; if (! defined $is_cached) { $message ||= qq{Request to url '$url' failed}; return ok(0 => $message); } $message ||= qq{Request to $url should not be cached by Varnish}; return ok(! $is_cached, $message); } sub _is_cached { my ($self, $args) = @_; if (! $args || ref $args ne 'HASH') { croak q{is_cached() requires a hashref}; } # 'headers' is optional, 'url' is mandatory if (! $args->{url}) { croak q{is_cached() requires a 'url'}; } my $res = $self->request($args); # Request failed, assert a test failure if (! $res) { return; } # Request successful, check if varnish has cached it return $self->analyze_response($res); } sub request { my ($self, $args) = @_; my $method = $args->{method} || q(GET); my $headers = $args->{headers} || {}; my $url = $args->{url}; if (! $url) { croak(q(No 'url' argument?)); } #if (! exists $headers->{Host} || ! $headers->{Host}) { # croak(q(No 'host' header?)); #} # Init user agent object my $ua = $self->user_agent(); # Avoid the '//' or varnish rules don't fire properly my $host = $headers->{Host}; if (! $host) { my $url_obj = URI->new($url); if (! $url_obj) { croak(qq(URI failed parsing url '$url'. Can't continue without a "Host" header.)); } $host = $url_obj->host(); } my $req = HTTP::Request->new($method => $url); # We need to set HTTP/1.1 Host: header or the varnish # rules based on hostname won't kick in (my.cn. vs my.) $req->header(Host => $host); if ($headers) { while (my ($name, $value) = each %{ $headers }) { if ($name eq 'Cookie') { ($name, $value) = split '=', $value, 2; #diag ("Setting cookie [$name] => [$value]"); $ua->cookie_jar->set_cookie(undef, $name, $value, '/', $host); #$req->header(Cookie => "$name=$value"); } else { $req->header($name => $value); } } } my $res = $ua->request($req); #if ($headers && exists $headers->{Cookie}) { # $ua->cookie_jar->clear_temporary_cookies(); #} return $res; } sub _response_sets_cookies { my ($res) = @_; my $cookie_header = $res->header("Set-Cookie"); #diag("cookie_header: " . ($cookie_header || "")); return defined $cookie_header && $cookie_header ne q{} ? 1 : 0; } sub user_agent { my ($self) = @_; my $ua = LWP::UserAgent->new( max_redirect => 0 ); my $jar = HTTP::Cookies->new(); $ua->agent($self->user_agent_string()); $ua->cookie_jar($jar); return $ua; } sub user_agent_string { return qq{Test-Varnish/$VERSION}; } sub verbose { my $self = shift; if (@_) { $self->{_verbose} = shift(@_) ? 1 : 0; } return $self->{_verbose}; } 1; # End of Test::Varnish __END__ =pod =head1 NAME Test::Varnish - Put your Varnish server to the test! =head1 VERSION Version 0.03 =head1 SYNOPSIS Varnish is a high performance reverse proxy. This module allows you to perform tests against a varnish server, asserting that a given resource (URL) is cached by varnish or not. See it as a sort of C<Test::More> extension to test Varnish. This can be useful when you want to test that your varnish setup and configuration works as expected. Another use for this module would be to poll random webservers to discover who is using Varnish. use Test::Varnish; plan tests => 2; my $test_client = Test::Varnish->new({ verbose => 1 }); $test_client->isnt_cached( { url => 'http://my.opera.com/community/', }, 'My Opera frontpage is not (yet) cached by Varnish' ); $test_client->is_cached( { url => 'http://www.cnn.com/', }, 'Is CNN.com using Varnish?' ); =head1 FUNCTIONS =head2 new Class constructor. Allows you to create a C<Test::Varnish> object. The allowed options are: =over 4 =item C<verbose> Controls the B<verbose> mode, where additional diagnostic messages (not many, actually) are output together with the test assertions. Set it to a true value to enable, false to disable. =back =head3 Example use Test::Varnish; my $tv = Test::Varnish->new(); or use Test::Varnish; my $tv = Test::Varnish->new({ verbose => 1 }); =head1 METHODS =head2 analyze_response Takes an L<HTTP::Response> object as argument. Examines the response headers to look for the default Varnish header (C<X-Varnish>), to tell you if the response was coming directly from the Varnish cache, or not. In other words, this tells you if the request was a Varnish cache hit or miss. =head2 is_cached C<is_cached()> is a test assertion. Asserts that a given request to a URL with certain headers, and such, is cached by the given Varnish instance. Needs 2 arguments: =over =item * C<\%request> Request data, as hashref. You can specify: =over =item C<url> The URL where to send the request to =item C<headers> Additional HTTP headers, as hashref. See the example. Most probably you will need the C<Host> header for Varnish to direct the request to the appropriate backend. YMMV. =back =item * C<$message> (optional) A message for the test assertion (ex.: C<Request to the frontpage with cookies should not be cached>). A default message will be provided if none is passed. =back =head3 Example use Test::Varnish; my $tv = Test::Varnish->new(); $tv->is_cached( { url => 'http://your-server.your-domain.local', headers => { Host => 'www.your-domain.local', # ... } } ); or: use Test::Varnish; my $tv = Test::Varnish->new(); $tv->is_cached( { url => 'http://192.168.1.100/super/', headers => { 'Host' => 'www.your-domain.local', 'Accept-Language' => 'it', } }, 'The super pages should always be cached, also in italian', ); =head2 isnt_cached C<isnt_cached()> is a test assertion, exactly the opposite of L</is_cached>. Asserts that a given request to a URL is B<not cached> by the queried Varnish instance. =head2 user_agent Returns a suitable user agent object (currently an L<LWP::UserAgent> instance), that can be used to interact with the varnish instance. =head2 user_agent_string Defines the default user agent string to be used for the requests issued by the default user agent object returned by L</user_agent>. You can subclass C<Test::Varnish> to define your own user agent string. I'm not sure this is 100% reasonable. Maybe. =head2 verbose Used internally, tells us if we're running in verbose mode. When verbose mode is active, the test assertions methods will output a bunch of diagnostic messages through C<Test::More::diag()>. B<You can activate the verbose mode by saying>: my $tv = Test::Varnish->new(); $tv->verbose(1); Or, by instantiating the C<Test::Varnish> object with the C<verbose> option, giving it a true value: my $tv = Test::Varnish->new({ verbose => 1 }); =head1 AUTHOR Cosimo Streppone, C<< <cosimo at cpan.org> >> =head1 BUGS Please report any bugs or feature requests to C<bug-test-varnish at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Varnish>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc Test::Varnish You can also look for information at: =over 4 =item * RT: CPAN's request tracker L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Test-Varnish> =item * AnnoCPAN: Annotated CPAN documentation L<http://annocpan.org/dist/Test-Varnish> =item * CPAN Ratings L<http://cpanratings.perl.org/d/Test-Varnish> =item * Search CPAN L<http://search.cpan.org/dist/Test-Varnish> =back =head1 COPYRIGHT & LICENSE Copyright 2010 Cosimo Streppone, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut