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