File Coverage

blib/lib/Catalyst/Test.pm
Criterion Covered Total %
statement 128 152 84.2
branch 34 58 58.6
condition 7 14 50.0
subroutine 33 36 91.6
pod n/a
total 202 260 77.6


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