File Coverage

lib/Test/WWW/Mechanize/Catalyst.pm
Criterion Covered Total %
statement 93 113 82.3
branch 48 70 68.5
condition 16 23 69.5
subroutine 15 15 100.0
pod 1 2 50.0
total 173 223 77.5


line stmt bran cond sub pod time code
1             package Test::WWW::Mechanize::Catalyst;
2              
3 10     10   3631642 use Moose;
  10         3269911  
  10         71  
4              
5 10     10   69895 use Carp qw/croak/;
  10         51  
  10         921  
6             require Catalyst::Test; # Do not call import
7 10     10   76 use Class::Load qw(load_class is_class_loaded);
  10         17  
  10         477  
8 10     10   5050 use Encode qw();
  10         70039  
  10         255  
9 10     10   3815 use HTML::Entities;
  10         39293  
  10         848  
10 10     10   6398 use Test::WWW::Mechanize;
  10         1310403  
  10         13894  
11              
12             extends 'Test::WWW::Mechanize', 'Moose::Object';
13              
14             #use namespace::clean -except => 'meta';
15              
16             our $VERSION = '0.61';
17             our $APP_CLASS;
18             my $Test = Test::Builder->new();
19              
20             has catalyst_app => (
21             is => 'ro',
22             predicate => 'has_catalyst_app',
23             );
24              
25             has allow_external => (
26             is => 'rw',
27             isa => 'Bool',
28             default => 0
29             );
30              
31             has host => (
32             is => 'rw',
33             isa => 'Str',
34             clearer => 'clear_host',
35             predicate => 'has_host',
36             );
37              
38             sub new {
39 15     15 1 2005443 my $class = shift;
40              
41 15 50       161 my $args = ref $_[0] ? $_[0] : { @_ };
42            
43             # Dont let LWP complain about options for our attributes
44             my %attr_options = map {
45 15         173 my $n = $_->init_arg;
  45         3022  
46             defined $n && exists $args->{$n}
47 45 100 66     312 ? ( $n => delete $args->{$n} )
48             : ( );
49             } $class->meta->get_all_attributes;
50              
51 15         359 my $obj = $class->SUPER::new(%$args);
52 15 100       143081 my $self = $class->meta->new_object(
53             __INSTANCE__ => $obj,
54             ($APP_CLASS ? (catalyst_app => $APP_CLASS) : () ),
55             %attr_options
56             );
57              
58 15         2866323 $self->BUILDALL;
59              
60              
61 15         179 return $self;
62             }
63              
64             sub BUILD {
65 30     30 0 33513 my ($self) = @_;
66              
67 30 100       141 unless ($ENV{CATALYST_SERVER}) {
68 28 50       971 croak "catalyst_app attribute is required unless CATALYST_SERVER env variable is set"
69             unless $self->has_catalyst_app;
70 28 100       649 load_class($self->catalyst_app)
71             unless (is_class_loaded($self->catalyst_app));
72             }
73             }
74              
75             sub _make_request {
76 35     35   10201951 my ( $self, $request, $arg, $size, $previous) = @_;
77              
78 35         170 my $response = $self->_do_catalyst_request($request);
79 35 100       152983 $response->header( 'Content-Base', $response->request->uri )
80             unless $response->header('Content-Base');
81              
82 35 50       4179 $self->cookie_jar->extract_cookies($response) if $self->cookie_jar;
83              
84             # fail tests under the Catalyst debug screen
85 35 50 100     5256 if ( !$self->{catalyst_debug}
      66        
86             && $response->code == 500
87             && $response->content =~ /on Catalyst \d+\.\d+/ )
88             {
89 0         0 my ($error)
90             = ( $response->content =~ /<code class="error">(.*?)<\/code>/s );
91 0   0     0 $error ||= "unknown error";
92 0         0 decode_entities($error);
93 0         0 $Test->diag("Catalyst error screen: $error");
94 0         0 $response->content('');
95 0         0 $response->content_type('');
96             }
97              
98             # NOTE: cargo-culted redirect checking from LWP::UserAgent:
99 35 100       712 $response->previous($previous) if $previous;
100 35 50       214 my $redirects = defined $response->redirects ? $response->redirects : 0;
101 35 100 100     1547 if ($redirects > 0 and $redirects >= $self->max_redirect) {
102 1         22 return $self->_redirect_loop_detected($response);
103             }
104              
105             # check if that was a redirect
106 34 100 100     240 if ( $response->header('Location')
      66        
107             && $response->is_redirect
108             && $self->redirect_ok( $request, $response ) )
109             {
110 6 100       929 return $self->_redirect_loop_detected($response) if $self->max_redirect <= 0;
111              
112             # TODO: this should probably create the request by cloning the original
113             # request and modifying it as LWP::UserAgent::request does. But for now...
114              
115             # *where* do they want us to redirect to?
116 5         57 my $location = $response->header('Location');
117              
118             # no-one *should* be returning non-absolute URLs, but if they
119             # are then we'd better cope with it. Let's create a new URI, using
120             # our request as the base.
121 5         194 my $uri = URI->new_abs( $location, $request->uri )->as_string;
122 5         1109 my $referral = HTTP::Request->new( GET => $uri );
123 5         510 return $self->request( $referral, $arg, $size, $response );
124             } else {
125 28         1305 $response->{_raw_content} = $response->content;
126             }
127              
128 28         591 return $response;
129             }
130              
131             sub _redirect_loop_detected {
132 2     2   17 my ( $self, $response ) = @_;
133 2         7 $response->header("Client-Warning" =>
134             "Redirect loop detected (max_redirect = " . $self->max_redirect . ")");
135 2         211 $response->{_raw_content} = $response->content;
136 2         32 return $response;
137             }
138              
139             sub _set_host_header {
140 36     36   103 my ( $self, $request ) = @_;
141             # If there's no Host header, set one.
142 36 50       119 unless ($request->header('Host')) {
143 36 100       2715 my $host = $self->has_host
144             ? $self->host
145             : $request->uri->host;
146 36 100       1079 $host .= ':'.$request->uri->_port if $request->uri->_port;
147 36         915 $request->header('Host', $host);
148             }
149             }
150              
151             sub _do_catalyst_request {
152 36     36   1983 my ($self, $request) = @_;
153              
154 36         108 my $uri = $request->uri;
155 36 100       406 $uri->scheme('http') unless defined $uri->scheme;
156 36 100       2516 $uri->host('localhost') unless defined $uri->host;
157              
158 36         3148 $request = $self->prepare_request($request);
159 36 50       21398 $self->cookie_jar->add_cookie_header($request) if $self->cookie_jar;
160              
161             # Woe betide anyone who unsets CATALYST_SERVER
162             return $self->_do_remote_request($request)
163 36 100       6085 if $ENV{CATALYST_SERVER};
164              
165 31         155 $self->_set_host_header($request);
166              
167 31         1458 my $res = $self->_check_external_request($request);
168 31 50       101 return $res if $res;
169              
170 31         209 my @creds = $self->get_basic_credentials( "Basic", $uri );
171 31 100       2662 $request->authorization_basic( @creds ) if @creds;
172              
173 31         1748 require Catalyst;
174             my $response = $Catalyst::VERSION >= 5.89000 ?
175             Catalyst::Test::_local_request($self->{catalyst_app}, $request) :
176 31 50       317 Catalyst::Test::local_request($self->{catalyst_app}, $request);
177              
178              
179             # LWP would normally do this, but we don't get down that far.
180 31         764645 $response->request($request);
181              
182 31         363 return $response
183             }
184              
185             sub _check_external_request {
186 36     36   102 my ($self, $request) = @_;
187              
188             # If there's no host then definitley not an external request.
189 36 50       117 $request->uri->can('host_port') or return;
190              
191 36 50 33     1499 if ( $self->allow_external && $request->uri->host_port ne 'localhost:80' ) {
192 0         0 return $self->SUPER::_make_request($request);
193             }
194 36         94 return undef;
195             }
196              
197             sub _do_remote_request {
198 5     5   17 my ($self, $request) = @_;
199              
200 5         22 my $res = $self->_check_external_request($request);
201 5 50       13 return $res if $res;
202              
203 5         21 my $server = URI->new( $ENV{CATALYST_SERVER} );
204              
205 5 50       426 if ( $server->path =~ m|^(.+)?/$| ) {
206 0         0 my $path = $1;
207 0 0       0 $server->path("$path") if $path; # need to be quoted
208             }
209              
210             # the request path needs to be sanitised if $server is using a
211             # non-root path due to potential overlap between request path and
212             # response path.
213 5 50       86 if ($server->path) {
214             # If request path is '/', we have to add a trailing slash to the
215             # final request URI
216 0         0 my $add_trailing = $request->uri->path eq '/';
217            
218 0         0 my @sp = split '/', $server->path;
219 0         0 my @rp = split '/', $request->uri->path;
220 0         0 shift @sp;shift @rp; # leading /
  0         0  
221 0 0       0 if (@rp) {
222 0         0 foreach my $sp (@sp) {
223 0 0       0 $sp eq $rp[0] ? shift @rp : last
224             }
225             }
226 0         0 $request->uri->path(join '/', @rp);
227            
228 0 0       0 if ( $add_trailing ) {
229 0         0 $request->uri->path( $request->uri->path . '/' );
230             }
231             }
232              
233 5         63 $request->uri->scheme( $server->scheme );
234 5         597 $request->uri->host( $server->host );
235 5         767 $request->uri->port( $server->port );
236 5         491 $request->uri->path( $server->path . $request->uri->path );
237 5         338 $self->_set_host_header($request);
238 5         360 return $self->SUPER::_make_request($request);
239             }
240              
241             sub import {
242 10     10   104 my ($class, $app) = @_;
243              
244 10 100       8949 if (defined $app) {
245 7 100       90 load_class($app)
246             unless (is_class_loaded($app));
247 7         10764504 $APP_CLASS = $app;
248             }
249              
250             }
251              
252              
253             1;
254              
255             __END__
256              
257             =head1 NAME
258              
259             Test::WWW::Mechanize::Catalyst - Test::WWW::Mechanize for Catalyst
260              
261             =head1 SYNOPSIS
262              
263             # We're in a t/*.t test script...
264             use Test::WWW::Mechanize::Catalyst;
265              
266             # To test a Catalyst application named 'Catty':
267             my $mech = Test::WWW::Mechanize::Catalyst->new(catalyst_app => 'Catty');
268              
269             $mech->get_ok("/"); # no hostname needed
270             is($mech->ct, "text/html");
271             $mech->title_is("Root", "On the root page");
272             $mech->content_contains("This is the root page", "Correct content");
273             $mech->follow_link_ok({text => 'Hello'}, "Click on Hello");
274             # ... and all other Test::WWW::Mechanize methods
275            
276             # White label site testing
277             $mech->host("foo.com");
278             $mech->get_ok("/");
279              
280             =head1 DESCRIPTION
281              
282             L<Catalyst> is an elegant MVC Web Application Framework.
283             L<Test::WWW::Mechanize> is a subclass of L<WWW::Mechanize> that incorporates
284             features for web application testing. The L<Test::WWW::Mechanize::Catalyst>
285             module meshes the two to allow easy testing of L<Catalyst> applications without
286             needing to start up a web server.
287              
288             Testing web applications has always been a bit tricky, normally
289             requiring starting a web server for your application and making real HTTP
290             requests to it. This module allows you to test L<Catalyst> web
291             applications but does not require a server or issue HTTP
292             requests. Instead, it passes the HTTP request object directly to
293             L<Catalyst>. Thus you do not need to use a real hostname:
294             "http://localhost/" will do. However, this is optional. The following
295             two lines of code do exactly the same thing:
296              
297             $mech->get_ok('/action');
298             $mech->get_ok('http://localhost/action');
299              
300             Links which do not begin with / or are not for localhost can be handled
301             as normal Web requests - this is handy if you have an external
302             single sign-on system. You must set allow_external to true for this:
303              
304             $mech->allow_external(1);
305              
306             You can also test a remote server by setting the environment variable
307             CATALYST_SERVER; for example:
308              
309             $ CATALYST_SERVER=http://example.com/myapp prove -l t
310              
311             will run the same tests on the application running at
312             http://example.com/myapp regardless of whether or not you specify
313             http:://localhost for Test::WWW::Mechanize::Catalyst.
314              
315             Furthermore, if you set CATALYST_SERVER, the server will be regarded
316             as a remote server even if your links point to localhost. Thus, you
317             can use Test::WWW::Mechanize::Catalyst to test your live webserver
318             running on your local machine, if you need to test aspects of your
319             deployment environment (for example, configuration options in an
320             http.conf file) instead of just the Catalyst request handling.
321            
322             This makes testing fast and easy. L<Test::WWW::Mechanize> provides
323             functions for common web testing scenarios. For example:
324              
325             $mech->get_ok( $page );
326             $mech->title_is( "Invoice Status", "Make sure we're on the invoice page" );
327             $mech->content_contains( "Andy Lester", "My name somewhere" );
328             $mech->content_like( qr/(cpan|perl)\.org/, "Link to perl.org or CPAN" );
329              
330             This module supports cookies automatically.
331              
332             To use this module you must pass it the name of the application. See
333             the SYNOPSIS above.
334              
335             Note that Catalyst has a special development feature: the debug
336             screen. By default this module will treat responses which are the
337             debug screen as failures. If you actually want to test debug screens,
338             please use:
339              
340             $mech->{catalyst_debug} = 1;
341              
342             An alternative to this module is L<Catalyst::Test>.
343              
344             =head1 CONSTRUCTOR
345              
346             =head2 new
347              
348             Behaves like, and calls, L<WWW::Mechanize>'s C<new> method. Any params
349             passed in get passed to WWW::Mechanize's constructor. Note that we
350             need to pass the name of the Catalyst application to the "use":
351              
352             use Test::WWW::Mechanize::Catalyst 'Catty';
353             my $mech = Test::WWW::Mechanize::Catalyst->new;
354              
355             =head1 METHODS
356              
357             =head2 allow_external
358              
359             Links which do not begin with / or are not for localhost can be handled
360             as normal Web requests - this is handy if you have an external
361             single sign-on system. You must set allow_external to true for this:
362              
363             $mech->allow_external(1);
364              
365             head2 catalyst_app
366              
367             The name of the Catalyst app which we are testing against. Read-only.
368              
369             =head2 host
370              
371             The host value to set the "Host:" HTTP header to, if none is present already in
372             the request. If not set (default) then Catalyst::Test will set this to
373             localhost:80
374              
375             =head2 clear_host
376              
377             Unset the host attribute.
378              
379             =head2 has_host
380              
381             Do we have a value set for the host attribute
382              
383             =head2 $mech->get_ok($url, [ \%LWP_options ,] $desc)
384              
385             A wrapper around WWW::Mechanize's get(), with similar options, except the
386             second argument needs to be a hash reference, not a hash. Returns true or
387             false.
388              
389             =head2 $mech->title_is( $str [, $desc ] )
390              
391             Tells if the title of the page is the given string.
392              
393             $mech->title_is( "Invoice Summary" );
394              
395             =head2 $mech->title_like( $regex [, $desc ] )
396              
397             Tells if the title of the page matches the given regex.
398              
399             $mech->title_like( qr/Invoices for (.+)/
400              
401             =head2 $mech->title_unlike( $regex [, $desc ] )
402              
403             Tells if the title of the page does NOT match the given regex.
404              
405             $mech->title_unlike( qr/Invoices for (.+)/
406              
407             =head2 $mech->content_is( $str [, $desc ] )
408              
409             Tells if the content of the page matches the given string.
410              
411             =head2 $mech->content_contains( $str [, $desc ] )
412              
413             Tells if the content of the page contains I<$str>.
414              
415             =head2 $mech->content_lacks( $str [, $desc ] )
416              
417             Tells if the content of the page lacks I<$str>.
418              
419             =head2 $mech->content_like( $regex [, $desc ] )
420              
421             Tells if the content of the page matches I<$regex>.
422              
423             =head2 $mech->content_unlike( $regex [, $desc ] )
424              
425             Tells if the content of the page does NOT match I<$regex>.
426              
427             =head2 $mech->page_links_ok( [ $desc ] )
428              
429             Follow all links on the current page and test for HTTP status 200
430              
431             $mech->page_links_ok('Check all links');
432              
433             =head2 $mech->page_links_content_like( $regex,[ $desc ] )
434              
435             Follow all links on the current page and test their contents for I<$regex>.
436              
437             $mech->page_links_content_like( qr/foo/,
438             'Check all links contain "foo"' );
439              
440             =head2 $mech->page_links_content_unlike( $regex,[ $desc ] )
441              
442             Follow all links on the current page and test their contents do not
443             contain the specified regex.
444              
445             $mech->page_links_content_unlike(qr/Restricted/,
446             'Check all links do not contain Restricted');
447              
448             =head2 $mech->links_ok( $links [, $desc ] )
449              
450             Check the current page for specified links and test for HTTP status
451             200. The links may be specified as a reference to an array containing
452             L<WWW::Mechanize::Link> objects, an array of URLs, or a scalar URL
453             name.
454              
455             my @links = $mech->find_all_links( url_regex => qr/cnn\.com$/ );
456             $mech->links_ok( \@links, 'Check all links for cnn.com' );
457              
458             my @links = qw( index.html search.html about.html );
459             $mech->links_ok( \@links, 'Check main links' );
460              
461             $mech->links_ok( 'index.html', 'Check link to index' );
462              
463             =head2 $mech->link_status_is( $links, $status [, $desc ] )
464              
465             Check the current page for specified links and test for HTTP status
466             passed. The links may be specified as a reference to an array
467             containing L<WWW::Mechanize::Link> objects, an array of URLs, or a
468             scalar URL name.
469              
470             my @links = $mech->links();
471             $mech->link_status_is( \@links, 403,
472             'Check all links are restricted' );
473              
474             =head2 $mech->link_status_isnt( $links, $status [, $desc ] )
475              
476             Check the current page for specified links and test for HTTP status
477             passed. The links may be specified as a reference to an array
478             containing L<WWW::Mechanize::Link> objects, an array of URLs, or a
479             scalar URL name.
480              
481             my @links = $mech->links();
482             $mech->link_status_isnt( \@links, 404,
483             'Check all links are not 404' );
484              
485             =head2 $mech->link_content_like( $links, $regex [, $desc ] )
486              
487             Check the current page for specified links and test the content of
488             each against I<$regex>. The links may be specified as a reference to
489             an array containing L<WWW::Mechanize::Link> objects, an array of URLs,
490             or a scalar URL name.
491              
492             my @links = $mech->links();
493             $mech->link_content_like( \@links, qr/Restricted/,
494             'Check all links are restricted' );
495              
496             =head2 $mech->link_content_unlike( $links, $regex [, $desc ] )
497              
498             Check the current page for specified links and test that the content of each
499             does not match I<$regex>. The links may be specified as a reference to
500             an array containing L<WWW::Mechanize::Link> objects, an array of URLs,
501             or a scalar URL name.
502              
503             my @links = $mech->links();
504             $mech->link_content_like( \@links, qr/Restricted/,
505             'Check all links are restricted' );
506              
507             =head2 follow_link_ok( \%parms [, $comment] )
508              
509             Makes a C<follow_link()> call and executes tests on the results.
510             The link must be found, and then followed successfully. Otherwise,
511             this test fails.
512              
513             I<%parms> is a hashref containing the params to pass to C<follow_link()>.
514             Note that the params to C<follow_link()> are a hash whereas the parms to
515             this function are a hashref. You have to call this function like:
516              
517             $agent->follow_link_ok( {n=>3}, "looking for 3rd link" );
518              
519             As with other test functions, C<$comment> is optional. If it is supplied
520             then it will display when running the test harness in verbose mode.
521              
522             Returns true value if the specified link was found and followed
523             successfully. The HTTP::Response object returned by follow_link()
524             is not available.
525              
526             =head1 CAVEATS
527              
528             =head2 External Redirects and allow_external
529              
530             If you use non-fully qualified urls in your test scripts (i.e. anything without
531             a host, such as C<< ->get_ok( "/foo") >> ) and your app redirects to an
532             external URL, expect to be bitten once you come back to your application's urls
533             (it will try to request them on the remote server). This is due to a limitation
534             in WWW::Mechanize.
535              
536             One workaround for this is that if you are expecting to redirect to an external
537             site, clone the TWMC object and use the cloned object for the external
538             redirect.
539              
540              
541             =head1 SEE ALSO
542              
543             Related modules which may be of interest: L<Catalyst>,
544             L<Test::WWW::Mechanize>, L<WWW::Mechanize>.
545              
546             =head1 AUTHOR
547              
548             Ash Berlin C<< <ash@cpan.org> >> (current maintainer)
549              
550             Original Author: Leon Brocard, C<< <acme@astray.com> >>
551              
552             =head1 COPYRIGHT
553              
554             Copyright (C) 2005-9, Leon Brocard
555              
556             =head1 LICENSE
557              
558             This module is free software; you can redistribute it or modify it
559             under the same terms as Perl itself.
560