claim a version for release
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Test.pm
CommitLineData
fc7ec1d9 1package Catalyst::Test;
2
d9d04ded 3use strict;
4use warnings;
b474372a 5use Test::More ();
e8d0f69a 6
5203d720 7use Plack::Test;
a2f2cde9 8use Catalyst::Exception;
d837e1a7 9use Catalyst::Utils;
7dd4f037 10use Class::MOP;
87cbe5e6 11use Sub::Exporter;
2a2c99c6 12use Carp 'croak', 'carp';
87cbe5e6 13
a0655e3a 14sub _build_request_export {
15 my ($self, $args) = @_;
16
2a2c99c6 17 return sub { _remote_request(@_) }
a0655e3a 18 if $args->{remote};
e8d0f69a 19
87cbe5e6 20 my $class = $args->{class};
21
a0655e3a 22 # Here we should be failing right away, but for some stupid backcompat thing
23 # I don't quite remember we fail lazily here. Needs a proper deprecation and
24 # then removal.
25 return sub { croak "Must specify a test app: use Catalyst::Test 'TestApp'" }
26 unless $class;
27
28 Class::MOP::load_class($class) unless Class::MOP::is_class_loaded($class);
29 $class->import;
87cbe5e6 30
2a2c99c6 31 return sub { _local_request( $class, @_ ) };
a0655e3a 32}
87cbe5e6 33
a0655e3a 34sub _build_get_export {
35 my ($self, $args) = @_;
36 my $request = $args->{request};
87cbe5e6 37
a0655e3a 38 return sub { $request->(@_)->content };
39}
40sub _build_ctx_request_export {
41 my ($self, $args) = @_;
42 my ($class, $request) = @{ $args }{qw(class request)};
43
44 return sub {
702729f5 45 my $me = ref $self || $self;
269194b4 46
eede256e 47 # fail if ctx_request is being used against a remote server
269194b4 48 Catalyst::Exception->throw("$me only works with local requests, not remote")
49 if $ENV{CATALYST_SERVER};
50
eede256e 51 # check explicitly for the class here, or the Cat->meta call will blow
52 # up in our face
ba151d0d 53 Catalyst::Exception->throw("Must specify a test app: use Catalyst::Test 'TestApp'") unless $class;
54
eede256e 55 # place holder for $c after the request finishes; reset every time
56 # requests are done.
9c74923d 57 my $ctx_closed_over;
269194b4 58
eede256e 59 # hook into 'dispatch' -- the function gets called after all plugins
60 # have done their work, and it's an easy place to capture $c.
cf1fb734 61 my $meta = Class::MOP::get_metaclass_by_name($class);
ba151d0d 62 $meta->make_mutable;
63 $meta->add_after_method_modifier( "dispatch", sub {
9c74923d 64 $ctx_closed_over = shift;
ba151d0d 65 });
cf1fb734 66 $meta->make_immutable( replace_constructor => 1 );
94f74acd 67 Class::C3::reinitialize(); # Fixes RT#46459, I've failed to write a test for how/why, but it does.
eede256e 68
69 # do the request; C::T::request will know about the class name, and
70 # we've already stopped it from doing remote requests above.
a0655e3a 71 my $res = $args->{request}->( @_ );
269194b4 72
9c74923d 73 # Make sure not to leave a reference $ctx hanging around.
74 # This means that the context will go out of scope as soon as the
75 # caller disposes of it, rather than waiting till the next time
76 # that ctx_request is called. This can be important if your $ctx
77 # ends up with a reference to a shared resource or lock (for example)
78 # which you want to clean up in test teardown - if the $ctx is still
79 # closed over then you're stuffed...
80 my $ctx = $ctx_closed_over;
81 undef $ctx_closed_over;
82
9c74923d 83 return ( $res, $ctx );
269194b4 84 };
a0655e3a 85}
86
87my $build_exports = sub {
88 my ($self, $meth, $args, $defaults) = @_;
89 my $class = $args->{class};
90
91 my $request = $self->_build_request_export({
92 class => $class,
93 remote => $ENV{CATALYST_SERVER},
94 });
95
96 my $get = $self->_build_get_export({ request => $request });
97
98 my $ctx_request = $self->_build_ctx_request_export({
99 class => $class,
100 request => $request,
101 });
269194b4 102
87cbe5e6 103 return {
4fbc0e85 104 request => $request,
105 get => $get,
106 ctx_request => $ctx_request,
87cbe5e6 107 content_like => sub {
108 my $action = shift;
109 return Test::More->builder->like($get->($action),@_);
110 },
111 action_ok => sub {
112 my $action = shift;
901991e6 113 my $meth = $request->($action)->request->method;
114 my @args = @_ ? @_ : ("$meth $action returns successfully");
115 return Test::More->builder->ok($request->($action)->is_success,@args);
87cbe5e6 116 },
117 action_redirect => sub {
118 my $action = shift;
901991e6 119 my $meth = $request->($action)->request->method;
120 my @args = @_ ? @_ : ("$meth $action returns a redirect");
121 return Test::More->builder->ok($request->($action)->is_redirect,@args);
87cbe5e6 122 },
123 action_notfound => sub {
124 my $action = shift;
901991e6 125 my $meth = $request->($action)->request->method;
126 my @args = @_ ? @_ : ("$meth $action returns a 404");
127 return Test::More->builder->is_eq($request->($action)->code,404,@args);
87cbe5e6 128 },
129 contenttype_is => sub {
130 my $action = shift;
131 my $res = $request->($action);
132 return Test::More->builder->is_eq(scalar($res->content_type),@_);
133 },
134 };
e0a78010 135};
e8d0f69a 136
d9d04ded 137our $default_host;
6e6df63d 138
139{
140 my $import = Sub::Exporter::build_exporter({
e0a78010 141 groups => [ all => $build_exports ],
6e6df63d 142 into_level => 1,
143 });
144
d9d04ded 145
6e6df63d 146 sub import {
d9d04ded 147 my ($self, $class, $opts) = @_;
955d6da6 148 Carp::carp(
149qq{Importing Catalyst::Test without an application name is deprecated:\n
150Instead of saying: use Catalyst::Test;
151say: use Catalyst::Test (); # If you don't want to import a test app right now.
152or say: use Catalyst::Test 'MyApp'; # If you do want to import a test app.\n\n})
153 unless $class;
6e6df63d 154 $import->($self, '-all' => { class => $class });
d258fcb2 155 $opts = {} unless ref $opts eq 'HASH';
d9d04ded 156 $default_host = $opts->{default_host} if exists $opts->{default_host};
269194b4 157 return 1;
6e6df63d 158 }
159}
160
fc7ec1d9 161=head1 NAME
162
8d2fa70c 163Catalyst::Test - Test Catalyst Applications
fc7ec1d9 164
165=head1 SYNOPSIS
166
49faa307 167 # Helper
49faa307 168 script/test.pl
169
fc7ec1d9 170 # Tests
171 use Catalyst::Test 'TestApp';
26dd6d9f 172 my $content = get('index.html'); # Content as string
173 my $response = request('index.html'); # HTTP::Response object
4fbc0e85 174 my($res, $c) = ctx_request('index.html'); # HTTP::Response & context object
fc7ec1d9 175
2f381252 176 use HTTP::Request::Common;
177 my $response = request POST '/foo', [
178 bar => 'baz',
179 something => 'else'
180 ];
181
45374ac6 182 # Run tests against a remote server
21465c88 183 CATALYST_SERVER='http://localhost:3000/' prove -r -l lib/ t/
45374ac6 184
b6898a9f 185 use Catalyst::Test 'TestApp';
e8d0f69a 186 use Test::More tests => 1;
b6898a9f 187
188 ok( get('/foo') =~ /bar/ );
189
d9d04ded 190 # mock virtual hosts
191 use Catalyst::Test 'MyApp', { default_host => 'myapp.com' };
192 like( get('/whichhost'), qr/served by myapp.com/ );
193 like( get( '/whichhost', { host => 'yourapp.com' } ), qr/served by yourapp.com/ );
194 {
195 local $Catalyst::Test::default_host = 'otherapp.com';
196 like( get('/whichhost'), qr/served by otherapp.com/ );
197 }
198
fc7ec1d9 199=head1 DESCRIPTION
200
2f381252 201This module allows you to make requests to a Catalyst application either without
202a server, by simulating the environment of an HTTP request using
203L<HTTP::Request::AsCGI> or remotely if you define the CATALYST_SERVER
0eb98177 204environment variable. This module also adds a few Catalyst-specific
205testing methods as displayed in the method section.
2f381252 206
f98f669b 207The L<get|/"$content = get( ... )"> and L<request|/"$res = request( ... );">
208functions take either a URI or an L<HTTP::Request> object.
fc7ec1d9 209
5f2e949d 210=head1 INLINE TESTS WILL NO LONGER WORK
211
965f3e35 212While it used to be possible to inline a whole test app into a C<.t> file for
213a distribution, this will no longer work.
5f2e949d 214
215The convention is to place your L<Catalyst> test apps into C<t/lib> in your
216distribution. E.g.: C<t/lib/TestApp.pm>, C<t/lib/TestApp/Controller/Root.pm>,
217etc.. Multiple test apps can be used in this way.
218
219Then write your C<.t> files like so:
220
221 use strict;
222 use warnings;
223 use FindBin '$Bin';
224 use lib "$Bin/lib";
225 use Test::More tests => 6;
226 use Catalyst::Test 'TestApp';
227
03f7a71b 228=head1 METHODS
fc7ec1d9 229
26dd6d9f 230=head2 $content = get( ... )
fc7ec1d9 231
232Returns the content.
233
234 my $content = get('foo/bar?test=1');
235
f13fc03f 236Note that this method doesn't follow redirects, so to test for a
237correctly redirecting page you'll need to use a combination of this
f98f669b 238method and the L<request|/"$res = request( ... );"> method below:
f13fc03f 239
240 my $res = request('/'); # redirects to /y
241 warn $res->header('location');
242 use URI;
243 my $uri = URI->new($res->header('location'));
244 is ( $uri->path , '/y');
245 my $content = get($uri->path);
246
8fa9321c 247Note also that the content is returned as raw bytes, without any attempt
248to decode it into characters.
249
26dd6d9f 250=head2 $res = request( ... );
fc7ec1d9 251
0eb98177 252Returns an L<HTTP::Response> object. Accepts an optional hashref for request
d9d04ded 253header configuration; currently only supports setting 'host' value.
fc7ec1d9 254
795117cf 255 my $res = request('foo/bar?test=1');
d9d04ded 256 my $virtual_res = request('foo/bar?test=1', {host => 'virtualhost.com'});
fc7ec1d9 257
f2e13bbd 258=head2 ($res, $c) = ctx_request( ... );
26dd6d9f 259
f98f669b 260Works exactly like L<request|/"$res = request( ... );">, except it also returns the Catalyst context object,
51a75afc 261C<$c>. Note that this only works for local requests.
26dd6d9f 262
0f895006 263=cut
264
aa3f9f53 265sub _request {
266 my $args = shift;
0f895006 267
5203d720 268 my $request = Catalyst::Utils::request(shift);
aa3f9f53 269
65791fc5 270 my %extra_env;
271 _customize_request($request, \%extra_env, @_);
aa3f9f53 272 $args->{mangle_request}->($request) if $args->{mangle_request};
5203d720 273
274 my $ret;
65791fc5 275 test_psgi
aa3f9f53 276 %{ $args },
277 app => sub { $args->{app}->({ %{ $_[0] }, %extra_env }) },
9c74923d 278 client => sub {
aa3f9f53 279 my ($psgi_app) = @_;
de1a65a7 280 my $resp = $psgi_app->($request);
aa3f9f53 281 $args->{mangle_response}->($resp) if $args->{mangle_response};
282 $ret = $resp;
283 };
284
285 return $ret;
286}
287
288sub _local_request {
289 my $class = shift;
290
291 return _request({
292 app => ref($class) eq "CODE" ? $class : $class->_finalized_psgi_app,
293 mangle_response => sub {
294 my ($resp) = @_;
9c74923d 295
296 # HTML head parsing based on LWP::UserAgent
297 #
6664017d 298 # This is because if you make a remote request with LWP, then the
299 # <BASE HREF="..."> from the returned HTML document will be used
300 # to fill in $res->base, as documented in HTTP::Response. We need
301 # to support this in local test requests so that they work 'the same'.
302 #
9c74923d 303 # This is not just horrible and possibly broken, but also really
304 # doesn't belong here. Whoever wants this should be working on
305 # getting it into Plack::Test, or make a middleware out of it, or
306 # whatever. Seriously - horrible.
307
308 require HTML::HeadParser;
309
310 my $parser = HTML::HeadParser->new();
311 $parser->xml_mode(1) if $resp->content_is_xhtml;
312 $parser->utf8_mode(1) if $] >= 5.008 && $HTML::Parser::VERSION >= 3.40;
313
314 $parser->parse( $resp->content );
315 my $h = $parser->header;
316 for my $f ( $h->header_field_names ) {
317 $resp->init_header( $f, [ $h->header($f) ] );
318 }
89222c2a 319 # Another horrible hack to make the response headers have a
320 # 'status' field. This is for back-compat, but you should
321 # call $resp->code instead!
322 $resp->init_header('status', [ $resp->code ]);
aa3f9f53 323 },
324 }, @_);
0f895006 325}
326
523d44ec 327my $agent;
328
2a2c99c6 329sub _remote_request {
68eb5874 330 require LWP::UserAgent;
555c9ff7 331 local $Plack::Test::Impl = 'ExternalServer';
68eb5874 332
68eb5874 333 unless ($agent) {
d837e1a7 334 $agent = LWP::UserAgent->new(
523d44ec 335 keep_alive => 1,
336 max_redirect => 0,
337 timeout => 60,
0eb98177 338
d11e0c1d 339 # work around newer LWP max_redirect 0 bug
340 # http://rt.cpan.org/Ticket/Display.html?id=40260
341 requests_redirectable => [],
523d44ec 342 );
d837e1a7 343
523d44ec 344 $agent->env_proxy;
345 }
45374ac6 346
555c9ff7 347
aa3f9f53 348 my $server = URI->new($ENV{CATALYST_SERVER});
349 if ( $server->path =~ m|^(.+)?/$| ) {
350 my $path = $1;
351 $server->path("$path") if $path; # need to be quoted
352 }
353
354 return _request({
355 ua => $agent,
356 uri => $server,
357 mangle_request => sub {
358 my ($request) = @_;
359
360 # the request path needs to be sanitised if $server is using a
361 # non-root path due to potential overlap between request path and
362 # response path.
363 if ($server->path) {
364 # If request path is '/', we have to add a trailing slash to the
365 # final request URI
366 my $add_trailing = ($request->uri->path eq '/' || $request->uri->path eq '') ? 1 : 0;
367
368 my @sp = split '/', $server->path;
369 my @rp = split '/', $request->uri->path;
370 shift @sp; shift @rp; # leading /
371 if (@rp) {
372 foreach my $sp (@sp) {
373 $sp eq $rp[0] ? shift @rp : last
374 }
375 }
376 $request->uri->path(join '/', @rp);
377
378 if ( $add_trailing ) {
379 $request->uri->path( $request->uri->path . '/' );
380 }
381 }
382 },
383 }, @_);
fc7ec1d9 384}
385
2a2c99c6 386for my $name (qw(local_request remote_request)) {
387 my $fun = sub {
388 carp <<"EOW";
389Calling Catalyst::Test::${name}() directly is deprecated.
390
391Please import Catalyst::Test into your namespace and use the provided request()
392function instead.
393EOW
394 return __PACKAGE__->can("_${name}")->(@_);
395 };
396
397 no strict 'refs';
398 *$name = $fun;
399}
400
d9d04ded 401sub _customize_request {
402 my $request = shift;
65791fc5 403 my $extra_env = shift;
d9d04ded 404 my $opts = pop(@_) || {};
4348c28b 405 $opts = {} unless ref($opts) eq 'HASH';
d9d04ded 406 if ( my $host = exists $opts->{host} ? $opts->{host} : $default_host ) {
407 $request->header( 'Host' => $host );
408 }
65791fc5 409
410 if (my $extra = $opts->{extra_env}) {
411 @{ $extra_env }{keys %{ $extra }} = values %{ $extra };
412 }
d9d04ded 413}
414
0bf3b41c 415=head2 action_ok($url [, $test_name ])
e8d0f69a 416
0bf3b41c 417Fetches the given URL and checks that the request was successful. An optional
418second argument can be given to specify the name of the test.
e8d0f69a 419
0bf3b41c 420=head2 action_redirect($url [, $test_name ])
e8d0f69a 421
0bf3b41c 422Fetches the given URL and checks that the request was a redirect. An optional
423second argument can be given to specify the name of the test.
e8d0f69a 424
0bf3b41c 425=head2 action_notfound($url [, $test_name ])
e8d0f69a 426
0bf3b41c 427Fetches the given URL and checks that the request was not found. An optional
428second argument can be given to specify the name of the test.
0eb98177 429
0bf3b41c 430=head2 content_like( $url, $regexp [, $test_name ] )
e8d0f69a 431
0bf3b41c 432Fetches the given URL and returns whether the content matches the regexp. An
433optional third argument can be given to specify the name of the test.
e8d0f69a 434
0bf3b41c 435=head2 contenttype_is($url, $type [, $test_name ])
e8d0f69a 436
0bf3b41c 437Verify the given URL has a content type of $type and optionally specify a test name.
e8d0f69a 438
fc7ec1d9 439=head1 SEE ALSO
440
2f381252 441L<Catalyst>, L<Test::WWW::Mechanize::Catalyst>,
442L<Test::WWW::Selenium::Catalyst>, L<Test::More>, L<HTTP::Request::Common>
fc7ec1d9 443
2f381252 444=head1 AUTHORS
fc7ec1d9 445
2f381252 446Catalyst Contributors, see Catalyst.pm
fc7ec1d9 447
448=head1 COPYRIGHT
449
536bee89 450This library is free software. You can redistribute it and/or modify it under
fc7ec1d9 451the same terms as Perl itself.
452
2a2c99c6 453=begin Pod::Coverage
454
455local_request
456
457remote_request
458
459=end Pod::Coverage
460
fc7ec1d9 461=cut
462
4631;