Merge branch 'master' into psgi
[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;
113 return Test::More->builder->ok($request->($action)->is_success, @_);
114 },
115 action_redirect => sub {
116 my $action = shift;
117 return Test::More->builder->ok($request->($action)->is_redirect,@_);
118 },
119 action_notfound => sub {
120 my $action = shift;
121 return Test::More->builder->is_eq($request->($action)->code,404,@_);
122 },
123 contenttype_is => sub {
124 my $action = shift;
125 my $res = $request->($action);
126 return Test::More->builder->is_eq(scalar($res->content_type),@_);
127 },
128 };
e0a78010 129};
e8d0f69a 130
d9d04ded 131our $default_host;
6e6df63d 132
133{
134 my $import = Sub::Exporter::build_exporter({
e0a78010 135 groups => [ all => $build_exports ],
6e6df63d 136 into_level => 1,
137 });
138
d9d04ded 139
6e6df63d 140 sub import {
d9d04ded 141 my ($self, $class, $opts) = @_;
955d6da6 142 Carp::carp(
143qq{Importing Catalyst::Test without an application name is deprecated:\n
144Instead of saying: use Catalyst::Test;
145say: use Catalyst::Test (); # If you don't want to import a test app right now.
146or say: use Catalyst::Test 'MyApp'; # If you do want to import a test app.\n\n})
147 unless $class;
6e6df63d 148 $import->($self, '-all' => { class => $class });
d258fcb2 149 $opts = {} unless ref $opts eq 'HASH';
d9d04ded 150 $default_host = $opts->{default_host} if exists $opts->{default_host};
269194b4 151 return 1;
6e6df63d 152 }
153}
154
fc7ec1d9 155=head1 NAME
156
8d2fa70c 157Catalyst::Test - Test Catalyst Applications
fc7ec1d9 158
159=head1 SYNOPSIS
160
49faa307 161 # Helper
49faa307 162 script/test.pl
163
fc7ec1d9 164 # Tests
165 use Catalyst::Test 'TestApp';
26dd6d9f 166 my $content = get('index.html'); # Content as string
167 my $response = request('index.html'); # HTTP::Response object
4fbc0e85 168 my($res, $c) = ctx_request('index.html'); # HTTP::Response & context object
fc7ec1d9 169
2f381252 170 use HTTP::Request::Common;
171 my $response = request POST '/foo', [
172 bar => 'baz',
173 something => 'else'
174 ];
175
45374ac6 176 # Run tests against a remote server
21465c88 177 CATALYST_SERVER='http://localhost:3000/' prove -r -l lib/ t/
45374ac6 178
b6898a9f 179 use Catalyst::Test 'TestApp';
e8d0f69a 180 use Test::More tests => 1;
b6898a9f 181
182 ok( get('/foo') =~ /bar/ );
183
d9d04ded 184 # mock virtual hosts
185 use Catalyst::Test 'MyApp', { default_host => 'myapp.com' };
186 like( get('/whichhost'), qr/served by myapp.com/ );
187 like( get( '/whichhost', { host => 'yourapp.com' } ), qr/served by yourapp.com/ );
188 {
189 local $Catalyst::Test::default_host = 'otherapp.com';
190 like( get('/whichhost'), qr/served by otherapp.com/ );
191 }
192
fc7ec1d9 193=head1 DESCRIPTION
194
2f381252 195This module allows you to make requests to a Catalyst application either without
196a server, by simulating the environment of an HTTP request using
197L<HTTP::Request::AsCGI> or remotely if you define the CATALYST_SERVER
0eb98177 198environment variable. This module also adds a few Catalyst-specific
199testing methods as displayed in the method section.
2f381252 200
f98f669b 201The L<get|/"$content = get( ... )"> and L<request|/"$res = request( ... );">
202functions take either a URI or an L<HTTP::Request> object.
fc7ec1d9 203
5f2e949d 204=head1 INLINE TESTS WILL NO LONGER WORK
205
206While it used to be possible to inline a whole testapp into a C<.t> file for a
207distribution, this will no longer work.
208
209The convention is to place your L<Catalyst> test apps into C<t/lib> in your
210distribution. E.g.: C<t/lib/TestApp.pm>, C<t/lib/TestApp/Controller/Root.pm>,
211etc.. Multiple test apps can be used in this way.
212
213Then write your C<.t> files like so:
214
215 use strict;
216 use warnings;
217 use FindBin '$Bin';
218 use lib "$Bin/lib";
219 use Test::More tests => 6;
220 use Catalyst::Test 'TestApp';
221
03f7a71b 222=head1 METHODS
fc7ec1d9 223
26dd6d9f 224=head2 $content = get( ... )
fc7ec1d9 225
226Returns the content.
227
228 my $content = get('foo/bar?test=1');
229
f13fc03f 230Note that this method doesn't follow redirects, so to test for a
231correctly redirecting page you'll need to use a combination of this
f98f669b 232method and the L<request|/"$res = request( ... );"> method below:
f13fc03f 233
234 my $res = request('/'); # redirects to /y
235 warn $res->header('location');
236 use URI;
237 my $uri = URI->new($res->header('location'));
238 is ( $uri->path , '/y');
239 my $content = get($uri->path);
240
8fa9321c 241Note also that the content is returned as raw bytes, without any attempt
242to decode it into characters.
243
26dd6d9f 244=head2 $res = request( ... );
fc7ec1d9 245
0eb98177 246Returns an L<HTTP::Response> object. Accepts an optional hashref for request
d9d04ded 247header configuration; currently only supports setting 'host' value.
fc7ec1d9 248
795117cf 249 my $res = request('foo/bar?test=1');
d9d04ded 250 my $virtual_res = request('foo/bar?test=1', {host => 'virtualhost.com'});
fc7ec1d9 251
f2e13bbd 252=head2 ($res, $c) = ctx_request( ... );
26dd6d9f 253
f98f669b 254Works exactly like L<request|/"$res = request( ... );">, except it also returns the Catalyst context object,
51a75afc 255C<$c>. Note that this only works for local requests.
26dd6d9f 256
0f895006 257=cut
258
2a2c99c6 259sub _local_request {
de1a65a7 260 my $class = shift;
261
8f076801 262 my $app = ref($class) eq "CODE" ? $class : $class->_finalized_psgi_app;
0f895006 263
5203d720 264 my $request = Catalyst::Utils::request(shift);
65791fc5 265 my %extra_env;
266 _customize_request($request, \%extra_env, @_);
5203d720 267
268 my $ret;
65791fc5 269 test_psgi
d87ef823 270 app => sub { $app->({ %{ $_[0] }, %extra_env }) },
9c74923d 271 client => sub {
de1a65a7 272 my $psgi_app = shift;
273
274 my $resp = $psgi_app->($request);
9c74923d 275
276 # HTML head parsing based on LWP::UserAgent
277 #
278 # This is not just horrible and possibly broken, but also really
279 # doesn't belong here. Whoever wants this should be working on
280 # getting it into Plack::Test, or make a middleware out of it, or
281 # whatever. Seriously - horrible.
282
283 require HTML::HeadParser;
284
285 my $parser = HTML::HeadParser->new();
286 $parser->xml_mode(1) if $resp->content_is_xhtml;
287 $parser->utf8_mode(1) if $] >= 5.008 && $HTML::Parser::VERSION >= 3.40;
288
289 $parser->parse( $resp->content );
290 my $h = $parser->header;
291 for my $f ( $h->header_field_names ) {
292 $resp->init_header( $f, [ $h->header($f) ] );
293 }
294
295 $ret = $resp;
296 };
5203d720 297
298 return $ret;
0f895006 299}
300
523d44ec 301my $agent;
302
2a2c99c6 303sub _remote_request {
68eb5874 304 require LWP::UserAgent;
305
d837e1a7 306 my $request = Catalyst::Utils::request( shift(@_) );
0f895006 307 my $server = URI->new( $ENV{CATALYST_SERVER} );
523d44ec 308
d9d04ded 309 _customize_request($request, @_);
310
523d44ec 311 if ( $server->path =~ m|^(.+)?/$| ) {
890e8d18 312 my $path = $1;
313 $server->path("$path") if $path; # need to be quoted
f4c0f6f7 314 }
cdae055a 315
316 # the request path needs to be sanitised if $server is using a
317 # non-root path due to potential overlap between request path and
318 # response path.
319 if ($server->path) {
f4c0f6f7 320 # If request path is '/', we have to add a trailing slash to the
321 # final request URI
322 my $add_trailing = $request->uri->path eq '/';
0eb98177 323
cdae055a 324 my @sp = split '/', $server->path;
325 my @rp = split '/', $request->uri->path;
326 shift @sp;shift @rp; # leading /
327 if (@rp) {
328 foreach my $sp (@sp) {
a7daf37e 329 $sp eq $rp[0] ? shift @rp : last
cdae055a 330 }
331 }
332 $request->uri->path(join '/', @rp);
0eb98177 333
f4c0f6f7 334 if ( $add_trailing ) {
335 $request->uri->path( $request->uri->path . '/' );
336 }
523d44ec 337 }
338
339 $request->uri->scheme( $server->scheme );
340 $request->uri->host( $server->host );
341 $request->uri->port( $server->port );
342 $request->uri->path( $server->path . $request->uri->path );
343
68eb5874 344 unless ($agent) {
9ffadf88 345
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
359 return $agent->request($request);
fc7ec1d9 360}
361
2a2c99c6 362for my $name (qw(local_request remote_request)) {
363 my $fun = sub {
364 carp <<"EOW";
365Calling Catalyst::Test::${name}() directly is deprecated.
366
367Please import Catalyst::Test into your namespace and use the provided request()
368function instead.
369EOW
370 return __PACKAGE__->can("_${name}")->(@_);
371 };
372
373 no strict 'refs';
374 *$name = $fun;
375}
376
d9d04ded 377sub _customize_request {
378 my $request = shift;
65791fc5 379 my $extra_env = shift;
d9d04ded 380 my $opts = pop(@_) || {};
4348c28b 381 $opts = {} unless ref($opts) eq 'HASH';
d9d04ded 382 if ( my $host = exists $opts->{host} ? $opts->{host} : $default_host ) {
383 $request->header( 'Host' => $host );
384 }
65791fc5 385
386 if (my $extra = $opts->{extra_env}) {
387 @{ $extra_env }{keys %{ $extra }} = values %{ $extra };
388 }
d9d04ded 389}
390
0bf3b41c 391=head2 action_ok($url [, $test_name ])
e8d0f69a 392
0bf3b41c 393Fetches the given URL and checks that the request was successful. An optional
394second argument can be given to specify the name of the test.
e8d0f69a 395
0bf3b41c 396=head2 action_redirect($url [, $test_name ])
e8d0f69a 397
0bf3b41c 398Fetches the given URL and checks that the request was a redirect. An optional
399second argument can be given to specify the name of the test.
e8d0f69a 400
0bf3b41c 401=head2 action_notfound($url [, $test_name ])
e8d0f69a 402
0bf3b41c 403Fetches the given URL and checks that the request was not found. An optional
404second argument can be given to specify the name of the test.
0eb98177 405
0bf3b41c 406=head2 content_like( $url, $regexp [, $test_name ] )
e8d0f69a 407
0bf3b41c 408Fetches the given URL and returns whether the content matches the regexp. An
409optional third argument can be given to specify the name of the test.
e8d0f69a 410
0bf3b41c 411=head2 contenttype_is($url, $type [, $test_name ])
e8d0f69a 412
0bf3b41c 413Verify the given URL has a content type of $type and optionally specify a test name.
e8d0f69a 414
fc7ec1d9 415=head1 SEE ALSO
416
2f381252 417L<Catalyst>, L<Test::WWW::Mechanize::Catalyst>,
418L<Test::WWW::Selenium::Catalyst>, L<Test::More>, L<HTTP::Request::Common>
fc7ec1d9 419
2f381252 420=head1 AUTHORS
fc7ec1d9 421
2f381252 422Catalyst Contributors, see Catalyst.pm
fc7ec1d9 423
424=head1 COPYRIGHT
425
536bee89 426This library is free software. You can redistribute it and/or modify it under
fc7ec1d9 427the same terms as Perl itself.
428
2a2c99c6 429=begin Pod::Coverage
430
431local_request
432
433remote_request
434
435=end Pod::Coverage
436
fc7ec1d9 437=cut
438
4391;