merge branch
[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
39ba6f9a 15our $VERSION = '0.55';
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;
136 $request->header('Host', $host);
137 }
138}
139
ab0b00e3 140sub _do_catalyst_request {
141 my ($self, $request) = @_;
142
dabec5e2 143 my $uri = $request->uri;
144 $uri->scheme('http') unless defined $uri->scheme;
145 $uri->host('localhost') unless defined $uri->host;
146
d6fc3a22 147 $request = $self->prepare_request($request);
ab0b00e3 148 $self->cookie_jar->add_cookie_header($request) if $self->cookie_jar;
149
150 # Woe betide anyone who unsets CATALYST_SERVER
97ae89ab 151 return $self->_do_remote_request($request)
ab0b00e3 152 if $ENV{CATALYST_SERVER};
153
39ba6f9a 154 $self->_set_host_header($request);
ee072621 155
97ae89ab 156 my $res = $self->_check_external_request($request);
157 return $res if $res;
158
ab0b00e3 159 my @creds = $self->get_basic_credentials( "Basic", $uri );
160 $request->authorization_basic( @creds ) if @creds;
161
ee072621 162 require Catalyst;
163 my $response = $Catalyst::VERSION >= 5.89000 ?
164 Catalyst::Test::_local_request($self->{catalyst_app}, $request) :
165 Catalyst::Test::local_request($self->{catalyst_app}, $request);
166
97ae89ab 167
168 # LWP would normally do this, but we dont get down that far.
169 $response->request($request);
170
171 return $response
172}
173
174sub _check_external_request {
175 my ($self, $request) = @_;
176
177 # If there's no host then definatley not an external request.
178 $request->uri->can('host_port') or return;
179
180 if ( $self->allow_external && $request->uri->host_port ne 'localhost:80' ) {
181 return $self->SUPER::_make_request($request);
182 }
183 return undef;
184}
185
186sub _do_remote_request {
187 my ($self, $request) = @_;
188
189 my $res = $self->_check_external_request($request);
190 return $res if $res;
191
192 my $server = URI->new( $ENV{CATALYST_SERVER} );
193
194 if ( $server->path =~ m|^(.+)?/$| ) {
195 my $path = $1;
196 $server->path("$path") if $path; # need to be quoted
197 }
198
199 # the request path needs to be sanitised if $server is using a
200 # non-root path due to potential overlap between request path and
201 # response path.
202 if ($server->path) {
203 # If request path is '/', we have to add a trailing slash to the
204 # final request URI
205 my $add_trailing = $request->uri->path eq '/';
206
207 my @sp = split '/', $server->path;
208 my @rp = split '/', $request->uri->path;
209 shift @sp;shift @rp; # leading /
210 if (@rp) {
211 foreach my $sp (@sp) {
212 $sp eq $rp[0] ? shift @rp : last
213 }
214 }
215 $request->uri->path(join '/', @rp);
216
217 if ( $add_trailing ) {
218 $request->uri->path( $request->uri->path . '/' );
219 }
220 }
221
222 $request->uri->scheme( $server->scheme );
223 $request->uri->host( $server->host );
224 $request->uri->port( $server->port );
225 $request->uri->path( $server->path . $request->uri->path );
39ba6f9a 226 $self->_set_host_header($request);
97ae89ab 227 return $self->SUPER::_make_request($request);
ab0b00e3 228}
229
6bc86362 230sub import {
254eca41 231 my ($class, $app) = @_;
f21ad406 232
254eca41 233 if (defined $app) {
f21ad406 234 Class::MOP::load_class($app)
235 unless (Class::MOP::is_class_loaded($app));
254eca41 236 $APP_CLASS = $app;
237 }
f21ad406 238
6bc86362 239}
240
6bc86362 241
2421;
243
244__END__
245
246=head1 NAME
247
248Test::WWW::Mechanize::Catalyst - Test::WWW::Mechanize for Catalyst
249
250=head1 SYNOPSIS
251
252 # We're in a t/*.t test script...
ab0b00e3 253 use Test::WWW::Mechanize::Catalyst;
254
6bc86362 255 # To test a Catalyst application named 'Catty':
ab0b00e3 256 my $mech = Test::WWW::Mechanize::Catalyst->new(catalyst_app => 'Catty');
6bc86362 257
6bc86362 258 $mech->get_ok("/"); # no hostname needed
259 is($mech->ct, "text/html");
260 $mech->title_is("Root", "On the root page");
261 $mech->content_contains("This is the root page", "Correct content");
262 $mech->follow_link_ok({text => 'Hello'}, "Click on Hello");
263 # ... and all other Test::WWW::Mechanize methods
ab0b00e3 264
265 # White label site testing
266 $mech->host("foo.com");
267 $mech->get_ok("/");
6bc86362 268
269=head1 DESCRIPTION
270
d6fc3a22 271L<Catalyst> is an elegant MVC Web Application Framework.
272L<Test::WWW::Mechanize> is a subclass of L<WWW::Mechanize> that incorporates
273features for web application testing. The L<Test::WWW::Mechanize::Catalyst>
274module meshes the two to allow easy testing of L<Catalyst> applications without
6675038c 275needing to start up a web server.
6bc86362 276
277Testing web applications has always been a bit tricky, normally
d6fc3a22 278requiring starting a web server for your application and making real HTTP
6bc86362 279requests to it. This module allows you to test L<Catalyst> web
d6fc3a22 280applications but does not require a server or issue HTTP
6bc86362 281requests. Instead, it passes the HTTP request object directly to
282L<Catalyst>. Thus you do not need to use a real hostname:
283"http://localhost/" will do. However, this is optional. The following
284two lines of code do exactly the same thing:
285
286 $mech->get_ok('/action');
287 $mech->get_ok('http://localhost/action');
288
289Links which do not begin with / or are not for localhost can be handled
290as normal Web requests - this is handy if you have an external
291single sign-on system. You must set allow_external to true for this:
292
d6fc3a22 293 $mech->allow_external(1);
6bc86362 294
295You can also test a remote server by setting the environment variable
d6fc3a22 296CATALYST_SERVER; for example:
6bc86362 297
298 $ CATALYST_SERVER=http://example.com/myapp prove -l t
299
300will run the same tests on the application running at
301http://example.com/myapp regardless of whether or not you specify
302http:://localhost for Test::WWW::Mechanize::Catalyst.
d6fc3a22 303
304Furthermore, if you set CATALYST_SERVER, the server will be regarded
305as a remote server even if your links point to localhost. Thus, you
306can use Test::WWW::Mechanize::Catalyst to test your live webserver
307running on your local machine, if you need to test aspects of your
308deployment environment (for example, configuration options in an
309http.conf file) instead of just the Catalyst request handling.
6bc86362 310
311This makes testing fast and easy. L<Test::WWW::Mechanize> provides
312functions for common web testing scenarios. For example:
313
314 $mech->get_ok( $page );
315 $mech->title_is( "Invoice Status", "Make sure we're on the invoice page" );
316 $mech->content_contains( "Andy Lester", "My name somewhere" );
317 $mech->content_like( qr/(cpan|perl)\.org/, "Link to perl.org or CPAN" );
318
319This module supports cookies automatically.
320
321To use this module you must pass it the name of the application. See
322the SYNOPSIS above.
323
6675038c 324Note that Catalyst has a special development feature: the debug
6bc86362 325screen. By default this module will treat responses which are the
326debug screen as failures. If you actually want to test debug screens,
327please use:
328
6675038c 329 $mech->{catalyst_debug} = 1;
6bc86362 330
331An alternative to this module is L<Catalyst::Test>.
332
333=head1 CONSTRUCTOR
334
335=head2 new
336
d6fc3a22 337Behaves like, and calls, L<WWW::Mechanize>'s C<new> method. Any params
6bc86362 338passed in get passed to WWW::Mechanize's constructor. Note that we
339need to pass the name of the Catalyst application to the "use":
340
341 use Test::WWW::Mechanize::Catalyst 'Catty';
342 my $mech = Test::WWW::Mechanize::Catalyst->new;
343
344=head1 METHODS
345
346=head2 allow_external
347
348Links which do not begin with / or are not for localhost can be handled
349as normal Web requests - this is handy if you have an external
350single sign-on system. You must set allow_external to true for this:
351
6675038c 352 $mech->allow_external(1);
6bc86362 353
1f8dbf85 354head2 catalyst_app
355
356The name of the Catalyst app which we are testing against. Read-only.
357
358=head2 host
359
360The host value to set the "Host:" HTTP header to, if none is present already in
361the request. If not set (default) then Catalyst::Test will set this to
362localhost:80
363
364=head2 clear_host
365
366Unset the host attribute.
367
368=head2 has_host
369
370Do we have a value set for the host attribute
371
6bc86362 372=head2 $mech->get_ok($url, [ \%LWP_options ,] $desc)
373
374A wrapper around WWW::Mechanize's get(), with similar options, except the
375second argument needs to be a hash reference, not a hash. Returns true or
376false.
377
378=head2 $mech->title_is( $str [, $desc ] )
379
380Tells if the title of the page is the given string.
381
382 $mech->title_is( "Invoice Summary" );
383
384=head2 $mech->title_like( $regex [, $desc ] )
385
386Tells if the title of the page matches the given regex.
387
388 $mech->title_like( qr/Invoices for (.+)/
389
390=head2 $mech->title_unlike( $regex [, $desc ] )
391
d6fc3a22 392Tells if the title of the page does NOT match the given regex.
6bc86362 393
394 $mech->title_unlike( qr/Invoices for (.+)/
395
396=head2 $mech->content_is( $str [, $desc ] )
397
6675038c 398Tells if the content of the page matches the given string.
6bc86362 399
400=head2 $mech->content_contains( $str [, $desc ] )
401
402Tells if the content of the page contains I<$str>.
403
404=head2 $mech->content_lacks( $str [, $desc ] )
405
406Tells if the content of the page lacks I<$str>.
407
408=head2 $mech->content_like( $regex [, $desc ] )
409
410Tells if the content of the page matches I<$regex>.
411
412=head2 $mech->content_unlike( $regex [, $desc ] )
413
414Tells if the content of the page does NOT match I<$regex>.
415
416=head2 $mech->page_links_ok( [ $desc ] )
417
418Follow all links on the current page and test for HTTP status 200
419
420 $mech->page_links_ok('Check all links');
421
422=head2 $mech->page_links_content_like( $regex,[ $desc ] )
423
424Follow all links on the current page and test their contents for I<$regex>.
425
426 $mech->page_links_content_like( qr/foo/,
427 'Check all links contain "foo"' );
428
429=head2 $mech->page_links_content_unlike( $regex,[ $desc ] )
430
431Follow all links on the current page and test their contents do not
432contain the specified regex.
433
434 $mech->page_links_content_unlike(qr/Restricted/,
435 'Check all links do not contain Restricted');
436
437=head2 $mech->links_ok( $links [, $desc ] )
438
439Check the current page for specified links and test for HTTP status
440200. The links may be specified as a reference to an array containing
441L<WWW::Mechanize::Link> objects, an array of URLs, or a scalar URL
442name.
443
444 my @links = $mech->find_all_links( url_regex => qr/cnn\.com$/ );
445 $mech->links_ok( \@links, 'Check all links for cnn.com' );
446
447 my @links = qw( index.html search.html about.html );
448 $mech->links_ok( \@links, 'Check main links' );
449
450 $mech->links_ok( 'index.html', 'Check link to index' );
451
452=head2 $mech->link_status_is( $links, $status [, $desc ] )
453
454Check the current page for specified links and test for HTTP status
455passed. The links may be specified as a reference to an array
456containing L<WWW::Mechanize::Link> objects, an array of URLs, or a
457scalar URL name.
458
459 my @links = $mech->links();
460 $mech->link_status_is( \@links, 403,
461 'Check all links are restricted' );
462
463=head2 $mech->link_status_isnt( $links, $status [, $desc ] )
464
465Check the current page for specified links and test for HTTP status
466passed. The links may be specified as a reference to an array
467containing L<WWW::Mechanize::Link> objects, an array of URLs, or a
468scalar URL name.
469
470 my @links = $mech->links();
471 $mech->link_status_isnt( \@links, 404,
472 'Check all links are not 404' );
473
474=head2 $mech->link_content_like( $links, $regex [, $desc ] )
475
476Check the current page for specified links and test the content of
477each against I<$regex>. The links may be specified as a reference to
478an array containing L<WWW::Mechanize::Link> objects, an array of URLs,
479or a scalar URL name.
480
481 my @links = $mech->links();
482 $mech->link_content_like( \@links, qr/Restricted/,
483 'Check all links are restricted' );
484
485=head2 $mech->link_content_unlike( $links, $regex [, $desc ] )
486
6675038c 487Check the current page for specified links and test that the content of each
6bc86362 488does not match I<$regex>. The links may be specified as a reference to
489an array containing L<WWW::Mechanize::Link> objects, an array of URLs,
490or 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 follow_link_ok( \%parms [, $comment] )
497
498Makes a C<follow_link()> call and executes tests on the results.
499The link must be found, and then followed successfully. Otherwise,
500this test fails.
501
d6fc3a22 502I<%parms> is a hashref containing the params to pass to C<follow_link()>.
503Note that the params to C<follow_link()> are a hash whereas the parms to
6bc86362 504this function are a hashref. You have to call this function like:
505
f319e68f 506 $agent->follow_link_ok( {n=>3}, "looking for 3rd link" );
6bc86362 507
508As with other test functions, C<$comment> is optional. If it is supplied
509then it will display when running the test harness in verbose mode.
510
511Returns true value if the specified link was found and followed
512successfully. The HTTP::Response object returned by follow_link()
513is not available.
514
532f2706 515=head1 CAVEATS
516
517=head2 External Redirects and allow_external
518
519If you use non-fully qualified urls in your test scripts (i.e. anything without
520a host, such as C<< ->get_ok( "/foo") >> ) and your app redirects to an
521external URL, expect to be bitten once you come back to your application's urls
6675038c 522(it will try to request them on the remote server). This is due to a limitation
532f2706 523in WWW::Mechanize.
524
525One workaround for this is that if you are expecting to redirect to an external
6675038c 526site, clone the TWMC object and use the cloned object for the external
532f2706 527redirect.
528
529
6bc86362 530=head1 SEE ALSO
531
532Related modules which may be of interest: L<Catalyst>,
533L<Test::WWW::Mechanize>, L<WWW::Mechanize>.
534
535=head1 AUTHOR
536
f21ad406 537Ash Berlin C<< <ash@cpan.org> >> (current maintiner)
254eca41 538
f21ad406 539Original Author: Leon Brocard, C<< <acme@astray.com> >>
6bc86362 540
541=head1 COPYRIGHT
542
6675038c 543Copyright (C) 2005-9, Leon Brocard
6bc86362 544
545=head1 LICENSE
546
547This module is free software; you can redistribute it or modify it
548under the same terms as Perl itself.
549