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