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             package Catalyst::Test;
2              
3 128     128   6705902 use strict;
  128         1253  
  128         4097  
4 128     128   732 use warnings;
  128         304  
  128         3435  
5 128     128   3947 use Test::More ();
  128         332453  
  128         2619  
6              
7 128     128   60430 use Plack::Test;
  128         138840  
  128         7100  
8 128     128   40503 use Catalyst::Exception;
  128         547  
  128         5018  
9 128     128   56852 use Catalyst::Utils;
  128         508  
  128         4961  
10 128     128   973 use Class::Load qw(load_class is_class_loaded);
  128         354  
  128         7335  
11 128     128   872 use Sub::Exporter;
  128         310  
  128         1130  
12 128     128   29339 use Moose::Util 'find_meta';
  128         326  
  128         780  
13 128     128   32300 use Carp 'croak', 'carp';
  128         356  
  128         268345  
14              
15             sub _build_request_export {
16 129     129   380 my ($self, $args) = @_;
17              
18 0     0   0 return sub { _remote_request(@_) }
19 129 50       493 if $args->{remote};
20              
21 129         312 my $class = $args->{class};
22              
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 2     2   1447 return sub { croak "Must specify a test app: use Catalyst::Test 'TestApp'" }
27 129 100       453 unless $class;
28              
29 127 100       1878 load_class($class) unless is_class_loaded($class);
30 127         13002 $class->import;
31              
32 127     912   1298 return sub { _local_request( $class, @_ ) };
  912         896200  
33             }
34              
35             sub _build_get_export {
36 129     129   572 my ($self, $args) = @_;
37 129         438 my $request = $args->{request};
38              
39 129     43   643 return sub { $request->(@_)->content };
  43         48609  
40             }
41             sub _build_ctx_request_export {
42 129     129   468 my ($self, $args) = @_;
43 129         432 my ($class, $request) = @{ $args }{qw(class request)};
  129         519  
44              
45             return sub {
46 79   33 79   108646 my $me = ref $self || $self;
        72      
47              
48             # fail if ctx_request is being used against a remote server
49             Catalyst::Exception->throw("$me only works with local requests, not remote")
50 79 50       366 if $ENV{CATALYST_SERVER};
51              
52             # check explicitly for the class here, or the Cat->meta call will blow
53             # up in our face
54 79 100       287 Catalyst::Exception->throw("Must specify a test app: use Catalyst::Test 'TestApp'") unless $class;
55              
56             # place holder for $c after the request finishes; reset every time
57             # requests are done.
58 78         159 my $ctx_closed_over;
59              
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.
62 78         355 my $meta = find_meta($class);
63 78         1737 $meta->make_mutable;
64             $meta->add_after_method_modifier( "dispatch", sub {
65 954     954   6837 $ctx_closed_over = shift;
66 78         19272 });
67 78         49906 $meta->make_immutable( replace_constructor => 1 );
68 78         35107 Class::C3::reinitialize(); # Fixes RT#46459, I've failed to write a test for how/why, but it does.
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.
72 78         567 my $res = $args->{request}->( @_ );
73              
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 78         219 my $ctx = $ctx_closed_over;
82 78         196 undef $ctx_closed_over;
83              
84 78         464 return ( $res, $ctx );
85 129         864 };
86             }
87              
88             my $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             });
103              
104             return {
105             request => $request,
106             get => $get,
107             ctx_request => $ctx_request,
108             content_like => sub {
109 9     9   5466 my $action = shift;
110 9         82 return Test::More->builder->like($get->($action),@_);
111             },
112             action_ok => sub {
113 3     3   1841 my $action = shift;
114 3         12 my $meth = $request->($action)->request->method;
115 3 100       117 my @args = @_ ? @_ : ("$meth $action returns successfully");
116 3         33 return Test::More->builder->ok($request->($action)->is_success,@args);
117             },
118             action_redirect => sub {
119 1     1   360 my $action = shift;
120 1         4 my $meth = $request->($action)->request->method;
121 1 50       44 my @args = @_ ? @_ : ("$meth $action returns a redirect");
122 1         8 return Test::More->builder->ok($request->($action)->is_redirect,@args);
123             },
124             action_notfound => sub {
125 2     2   1222 my $action = shift;
126 2         9 my $meth = $request->($action)->request->method;
127 2 100       75 my @args = @_ ? @_ : ("$meth $action returns a 404");
128 2         20 return Test::More->builder->is_eq($request->($action)->code,404,@args);
129             },
130             contenttype_is => sub {
131 1     1   527 my $action = shift;
132 1         6 my $res = $request->($action);
133 1         14 return Test::More->builder->is_eq(scalar($res->content_type),@_);
134             },
135             };
136             };
137              
138             our $default_host;
139              
140             {
141             my $import = Sub::Exporter::build_exporter({
142             groups => [ all => $build_exports ],
143             into_level => 1,
144             });
145              
146              
147             sub import {
148 129     129   4975 my ($self, $class, $opts) = @_;
149 129 100       1128 Carp::carp(
150             qq{Importing Catalyst::Test without an application name is deprecated:\n
151             Instead of saying: use Catalyst::Test;
152             say: use Catalyst::Test (); # If you don't want to import a test app right now.
153             or say: use Catalyst::Test 'MyApp'; # If you do want to import a test app.\n\n})
154             unless $class;
155 129         659 $import->($self, '-all' => { class => $class });
156 129 100       106167 $opts = {} unless ref $opts eq 'HASH';
157 129 100       744 $default_host = $opts->{default_host} if exists $opts->{default_host};
158 129         471782 return 1;
159             }
160             }
161              
162             =head1 NAME
163              
164             Catalyst::Test - Test Catalyst Applications
165              
166             =head1 SYNOPSIS
167              
168             # Helper
169             script/test.pl
170              
171             # Tests
172             use Catalyst::Test 'TestApp';
173             my $content = get('index.html'); # Content as string
174             my $response = request('index.html'); # HTTP::Response object
175             my($res, $c) = ctx_request('index.html'); # HTTP::Response & context object
176              
177             use HTTP::Request::Common;
178             my $response = request POST '/foo', [
179             bar => 'baz',
180             something => 'else'
181             ];
182              
183             # Run tests against a remote server
184             CATALYST_SERVER='http://localhost:3000/' prove -r -l lib/ t/
185              
186             use Catalyst::Test 'TestApp';
187             use Test::More tests => 1;
188              
189             ok( get('/foo') =~ /bar/ );
190              
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              
200             =head1 DESCRIPTION
201              
202             This module allows you to make requests to a Catalyst application either without
203             a server, by simulating the environment of an HTTP request using
204             L<HTTP::Request::AsCGI> or remotely if you define the CATALYST_SERVER
205             environment variable. This module also adds a few Catalyst-specific
206             testing methods as displayed in the method section.
207              
208             The L<get|/"$content = get( ... )"> and L<request|/"$res = request( ... );">
209             functions take either a URI or an L<HTTP::Request> object.
210              
211             =head1 INLINE TESTS WILL NO LONGER WORK
212              
213             While it used to be possible to inline a whole test app into a C<.t> file for
214             a distribution, this will no longer work.
215              
216             The convention is to place your L<Catalyst> test apps into C<t/lib> in your
217             distribution. E.g.: C<t/lib/TestApp.pm>, C<t/lib/TestApp/Controller/Root.pm>,
218             etc.. Multiple test apps can be used in this way.
219              
220             Then 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              
229             =head1 METHODS
230              
231             =head2 $content = get( ... )
232              
233             Returns the content.
234              
235             my $content = get('foo/bar?test=1');
236              
237             Note that this method doesn't follow redirects, so to test for a
238             correctly redirecting page you'll need to use a combination of this
239             method and the L<request|/"$res = request( ... );"> method below:
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              
248             Note also that the content is returned as raw bytes, without any attempt
249             to decode it into characters.
250              
251             =head2 $res = request( ... );
252              
253             Returns an L<HTTP::Response> object. Accepts an optional hashref for request
254             header configuration; currently only supports setting 'host' value.
255              
256             my $res = request('foo/bar?test=1');
257             my $virtual_res = request('foo/bar?test=1', {host => 'virtualhost.com'});
258              
259             Alternately, you can pass in an L<HTTP::Request::Common> object to set arbitrary
260             request headers.
261              
262             my $res = request(GET '/foo/bar',
263             X-Foo => 'Bar',
264             Authorization => 'Bearer JWT_HERE',
265             ...
266             );
267              
268             =head2 ($res, $c) = ctx_request( ... );
269              
270             Works exactly like L<request|/"$res = request( ... );">, except it also returns the Catalyst context object,
271             C<$c>. Note that this only works for local requests.
272              
273             =cut
274              
275             sub _request {
276 913     913   2529 my $args = shift;
277              
278 913         4158 my $request = Catalyst::Utils::request(shift);
279              
280 913         2285 my %extra_env;
281 913         4192 _customize_request($request, \%extra_env, @_);
282 913 50       2947 $args->{mangle_request}->($request) if $args->{mangle_request};
283              
284 913         1671 my $ret;
285             test_psgi
286 913         9957 %{ $args },
287 913     913   894947 app => sub { $args->{app}->({ %{ $_[0] }, %extra_env }) },
  913         16230  
288             client => sub {
289 913     913   896943 my ($psgi_app) = @_;
290 913         2764 my $resp = $psgi_app->($request);
291 912 50       120450 $args->{mangle_response}->($resp) if $args->{mangle_response};
292 912         71412 $ret = $resp;
293 913         1682 };
294              
295 912         20863 return $ret;
296             }
297              
298             sub _local_request {
299 913     913   2505 my $class = shift;
300              
301             return _request({
302             app => ref($class) eq "CODE" ? $class : $class->_finalized_psgi_app,
303             mangle_response => sub {
304 912     912   2679 my ($resp) = @_;
305              
306             # HTML head parsing based on LWP::UserAgent
307             #
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             #
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              
318 912 100 100     4399 if (!$resp->content_type || $resp->content_is_html) {
319 709         80973 require HTML::HeadParser;
320              
321 709         143492 my $parser = HTML::HeadParser->new();
322 709 50       82075 $parser->xml_mode(1) if $resp->content_is_xhtml;
323 709 50 33     29070 $parser->utf8_mode(1) if $] >= 5.008 && $HTML::Parser::VERSION >= 3.40;
324              
325 709         2424 $parser->parse( $resp->content );
326 709         28937 my $h = $parser->header;
327 709         5551 for my $f ( $h->header_field_names ) {
328 26         1344 $resp->init_header( $f, [ $h->header($f) ] );
329             }
330             }
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 912         38991 $resp->init_header('status', [ $resp->code ]);
335             },
336 913 50       8856 }, @_);
337             }
338              
339             my $agent;
340              
341             sub _remote_request {
342 0     0   0 require LWP::UserAgent;
343 0         0 local $Plack::Test::Impl = 'ExternalServer';
344              
345 0 0       0 unless ($agent) {
346 0         0 $agent = LWP::UserAgent->new(
347             keep_alive => 1,
348             max_redirect => 0,
349             timeout => 60,
350              
351             # work around newer LWP max_redirect 0 bug
352             # http://rt.cpan.org/Ticket/Display.html?id=40260
353             requests_redirectable => [],
354             );
355              
356 0         0 $agent->env_proxy;
357             }
358              
359              
360 0         0 my $server = URI->new($ENV{CATALYST_SERVER});
361 0 0       0 if ( $server->path =~ m|^(.+)?/$| ) {
362 0         0 my $path = $1;
363 0 0       0 $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 0     0   0 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 0 0       0 if ($server->path) {
376             # If request path is '/', we have to add a trailing slash to the
377             # final request URI
378 0 0 0     0 my $add_trailing = ($request->uri->path eq '/' || $request->uri->path eq '') ? 1 : 0;
379              
380 0         0 my @sp = split '/', $server->path;
381 0         0 my @rp = split '/', $request->uri->path;
382 0         0 shift @sp; shift @rp; # leading /
  0         0  
383 0 0       0 if (@rp) {
384 0         0 foreach my $sp (@sp) {
385 0 0       0 $sp eq $rp[0] ? shift @rp : last
386             }
387             }
388 0         0 $request->uri->path(join '/', @rp);
389              
390 0 0       0 if ( $add_trailing ) {
391 0         0 $request->uri->path( $request->uri->path . '/' );
392             }
393             }
394             },
395 0         0 }, @_);
396             }
397              
398             for my $name (qw(local_request remote_request)) {
399             my $fun = sub {
400 1     1   326 carp <<"EOW";
401             Calling Catalyst::Test::${name}() directly is deprecated.
402              
403             Please import Catalyst::Test into your namespace and use the provided request()
404             function instead.
405             EOW
406 1         24 return __PACKAGE__->can("_${name}")->(@_);
407             };
408              
409 128     128   1190 no strict 'refs';
  128         353  
  128         25525  
410             *$name = $fun;
411             }
412              
413             sub _customize_request {
414 918     918   2233 my $request = shift;
415 918         1959 my $extra_env = shift;
416 918   100     5212 my $opts = pop(@_) || {};
417 918 100       3860 $opts = {} unless ref($opts) eq 'HASH';
418 918 100       4517 if ( my $host = exists $opts->{host} ? $opts->{host} : $default_host ) {
    100          
419 27         131 $request->header( 'Host' => $host );
420             }
421              
422 918 100       5746 if (my $extra = $opts->{extra_env}) {
423 2         5 @{ $extra_env }{keys %{ $extra }} = values %{ $extra };
  2         19  
  2         4  
  2         8  
424             }
425             }
426              
427             =head2 action_ok($url [, $test_name ])
428              
429             Fetches the given URL and checks that the request was successful. An optional
430             second argument can be given to specify the name of the test.
431              
432             =head2 action_redirect($url [, $test_name ])
433              
434             Fetches the given URL and checks that the request was a redirect. An optional
435             second argument can be given to specify the name of the test.
436              
437             =head2 action_notfound($url [, $test_name ])
438              
439             Fetches the given URL and checks that the request was not found. An optional
440             second argument can be given to specify the name of the test.
441              
442             =head2 content_like( $url, $regexp [, $test_name ] )
443              
444             Fetches the given URL and returns whether the content matches the regexp. An
445             optional third argument can be given to specify the name of the test.
446              
447             =head2 contenttype_is($url, $type [, $test_name ])
448              
449             Verify the given URL has a content type of $type and optionally specify a test name.
450              
451             =head1 SEE ALSO
452              
453             L<Catalyst>, L<Test::WWW::Mechanize::Catalyst>,
454             L<Test::WWW::Selenium::Catalyst>, L<Test::More>, L<HTTP::Request::Common>
455              
456             =head1 AUTHORS
457              
458             Catalyst Contributors, see Catalyst.pm
459              
460             =head1 COPYRIGHT
461              
462             This library is free software. You can redistribute it and/or modify it under
463             the same terms as Perl itself.
464              
465             =begin Pod::Coverage
466              
467             local_request
468              
469             remote_request
470              
471             =end Pod::Coverage
472              
473             =cut
474              
475             1;