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