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