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