File Coverage

blib/lib/Test/WWW/Mechanize/Maypole.pm
Criterion Covered Total %
statement 40 109 36.7
branch 2 34 5.8
condition n/a
subroutine 13 18 72.2
pod 4 4 100.0
total 59 165 35.7


line stmt bran cond sub pod time code
1             package Test::WWW::Mechanize::Maypole;
2 1     1   35263 use strict;
  1         3  
  1         31  
3 1     1   6 use warnings;
  1         2  
  1         28  
4              
5 1     1   802 use HTTP::Status();
  1         3918  
  1         29  
6 1     1   835 use HTTP::Headers::Util;
  1         793  
  1         93  
7 1     1   762 use URI;
  1         7797  
  1         26  
8 1     1   834 use UNIVERSAL::require;
  1         1898  
  1         13  
9 1     1   1057 use NEXT;
  1         2324  
  1         8  
10              
11 1     1   1109 use Test::WWW::Mechanize;
  1         235150  
  1         23  
12 1     1   1701 use Class::Data::Inheritable;
  1         353  
  1         426  
13              
14 1     1   50 use base qw/ Test::WWW::Mechanize Class::Data::Inheritable /;
  1         3  
  1         275  
15              
16             __PACKAGE__->mk_classdata( '_the_app' );
17              
18             our $VERSION = '0.23';
19              
20             sub import
21             {
22 1     1   10 my ( $class, $app, @db_args ) = @_;
23            
24 1 50       5 if ( @db_args )
25             {
26 0         0 my $args = join ':', @db_args;
27            
28 0         0 eval "package $app;
29             sub setup { shift->NEXT::DISTINCT::setup( '$args' ) }"; # qw(@db_args) fails
30 0 0       0 die $@ if $@;
31             }
32            
33 1         4 $class->_the_app( $app );
34            
35 1 50       15 $app->require or die "Couldn't load Maypole app '$app': $@";
36            
37 0           my @exports = qw/ send_output parse_location get_template_root parse_args /;
38            
39 1     1   6 no strict 'refs';
  1         2  
  1         871  
40 0           *{"$app\::$_"} = \&$_ for @exports;
  0            
41             }
42              
43             =head1 NAME
44              
45             Test::WWW::Mechanize::Maypole - Test::WWW::Mechanize for Maypole
46              
47             =head1 SYNOPSIS
48              
49             use Test::WWW::Mechanize::Maypole 'BeerDB';
50            
51             # or load a test database instead of the one configured in BeerDB.pm:
52             #
53             # use Test::WWW::Mechanize::Maypole 'BeerDB', 'dbi:SQLite:test-beerdb.db';
54             # use Test::WWW::Mechanize::Maypole 'BeerDB', 'dbi:mysql:beer_d_b', 'dhoworth', 'password';
55            
56             $ENV{MAYPOLE_TEMPLATES} = 'path/to/templates';
57            
58             my $mech = Test::WWW::Mechanize::Maypole->new;
59            
60             #
61             # basic tests:
62             #
63             $mech->get_ok( "http://localhost/beerdb/" );
64            
65             is( $mech->ct, "text/html" );
66            
67             $mech->content_contains( 'This is the frontpage' );
68            
69             #
70             # logging in and storing cookies:
71             #
72             $mech->get_ok("http://localhost/beerdb/customer/buybeer");
73             $mech->content_contains( 'Login to BeerDB', 'got login page' );
74              
75             # specify which form we're interested in
76             $mech->form_number(1); # the 1st form
77            
78             # fill in credentials
79             $mech->field( 'username' => 'landlord' );
80             $mech->field( 'password' => 'handpump' );
81            
82             # get a HTTP::Response back
83             my $response = $mech->click_button( name => 'submit' );
84             like( $response->content, qr/Shop for beer/, 'got customer/buybeer page' );
85            
86             # check our cookies give access to other pages
87             $mech->get_ok( "http://localhost/beerdb/customer/edit" );
88             $mech->content_contains( 'Update your details', "got customer account edit page");
89              
90            
91             # ... see Test::WWW::Mechanize for many more test methods
92            
93             =head1 DESCRIPTION
94              
95             By inheriting from L, this module provides two key benefits
96             over using L in test scripts. First, it inherits a plethora of methods
97             for testing web content. Second, cookies are handled transparently, allowing
98             you to test applications that use cookie-based sessions and authentication.
99              
100             Testing web applications has always been a bit tricky, normally
101             starting a web server for your application and making real HTTP
102             requests to it. This module allows you to test L web
103             applications but does not start a server or issue HTTP
104             requests. Instead, it passes the HTTP request parameters directly to
105             L. Thus you do not need to use a real hostname:
106             "http://localhost/" will do.
107              
108             This makes testing fast and easy. L provides
109             functions for common web testing scenarios. For example:
110              
111             $mech->get_ok( $page );
112             $mech->title_is( "Invoice Status", "Make sure we're on the invoice page" );
113             $mech->content_contains( "David Baird", "My name somewhere" );
114             $mech->content_like( qr/(cpan|perl)\.org/, "Link to perl.org or CPAN" );
115              
116             This module supports cookies automatically.
117              
118             =head1 LOADING
119              
120             To use this module you must pass it the name of the application.
121              
122             Additionally, you can pass an alternate set of database connection parameters, and
123             these will override the settings configured in your application. Useful for connecting
124             to a test database without having to alter your production code. This won't work if
125             your application calls C inside a C block.
126              
127             =head1 CONSTRUCTOR
128              
129             =head2 new
130              
131             Inherited from L, which passes any parameters through to
132             L.
133              
134             Note that the name of the Maypole application should be passed to the C statement:
135              
136             use Test::WWW::Mechanize::Maypole 'BeerDB';
137             my $mech = Test::WWW::Mechanize::Maypole->new;
138            
139             =head1 ENVIRONMENT
140              
141             Set C<$ENV{MAYPOLE_TEMPLATES}> to the path where the templates for the application
142             can be found. Defaults to C<'.'>.
143              
144             =head1 METHODS
145              
146             Please see the documentation for L.
147              
148             =cut
149            
150             sub _make_request
151             {
152 0     0     my ( $self, $request ) = @_;
153            
154 0 0         $self->cookie_jar->add_cookie_header($request) if $self->cookie_jar;
155              
156             # make an HTTP::Response object, to be populated during the handler() call
157 0           my $response = HTTP::Response->new;
158 0           $response->date( time );
159            
160             # parse_location() normally takes the url from @ARGV, here we provide $request.
161             # $response is taken by send_output
162 0           local @ARGV = ( $request, $response );
163            
164             # handler() calls send_output with no args, so we provide $response via @ARGV
165 0           my $status = $self->_the_app->handler;
166            
167             # Translate Maypole codes to HTTP::Status codes. Maypole only has 2 codes, OK (0)
168             # and everything else (-1). We'll assume -1 is an error. Note that other codes can
169             # be returned by custom application code - we assume anything else is a proper
170             # HTTP status
171 0 0         if ( defined $status )
172             {
173 0 0         $status = 200 if $status == 0;
174 0 0         $status = 500 if $status == -1;
175             }
176             else
177             {
178 0           warn "Undefined response code";
179 0           $status = 500;
180             }
181              
182             # $response has now been populated during the handler() call
183 0           $response->code( $status );
184 0           $response->message( HTTP::Status::status_message( $status ) );
185            
186 0           $response->header( 'Content-Base', $request->uri );
187 0           $response->request( $request );
188            
189 0 0         $self->cookie_jar->extract_cookies($response) if $self->cookie_jar;
190            
191 0           return $response;
192             }
193              
194              
195             =head2 Exported methods
196              
197             These methods are exported into the application's namespace, and override methods that would
198             otherwise be inherited from Maypole or the Maypole frontend.
199              
200             You will not normally need to use these methods in your test scripts.
201              
202             If you need to replace these methods with custom versions, let me know, and I'll make exporting
203             more flexible.
204              
205             =over 4
206              
207             =item send_output
208              
209             =item parse_location
210              
211             =item parse_args
212              
213             =item get_template_root
214              
215             =back
216              
217             =cut
218              
219             # Called by Maypole::handler(), with no arguments, so $response is placed in @ARGV for
220             # retrieval here. This method, and _make_request, are the only places that use the
221             # $response object.
222              
223             # Grabs Maypole::Headers and populates the HTTP::Response object.
224             sub send_output
225             {
226 0     0 1   my ( $maypole ) = @_;
227            
228 0           my $response = shift @ARGV;
229            
230 0 0         $response->content_type(
231             $maypole->{content_type} =~ m/^text/
232             ? $maypole->{content_type} . "; charset=" . $maypole->{document_encoding}
233             : $maypole->{content_type}
234             );
235            
236 1     1   7 $response->content_length( do { use bytes; length $maypole->{output} } );
  1         2  
  1         10  
  0            
  0            
237              
238             # if there are cookies, this is where they get passed on
239 0           foreach ($maypole->headers_out->field_names)
240             {
241 0 0         next if /^Content-(Type|Length)/;
242 0           $response->header( $_ => $maypole->headers_out->get($_) );
243             }
244            
245 0           $response->content( $maypole->{output} );
246             }
247              
248             # Called by Maypole::handler() with no arguments.
249             sub parse_location
250             {
251 0     0 1   my ( $self ) = @_;
252            
253 0           my $request = shift @ARGV;
254            
255             # This is a HTTP::Headers object.
256 0           my $headers_in = $request->headers;
257            
258             # Maypole::Headers is a simple subclass of HTTP::Headers
259 0           bless $headers_in, 'Maypole::Headers';
260            
261 0           $self->headers_in( $headers_in );
262            
263 0           my $uri = $request->uri;
264            
265 0           ( my $uri_base = $self->config->uri_base ) =~ s:/$::;
266            
267 0           my $root = URI->new( $uri_base )->path;
268            
269 0           $self->{path} = $uri->path;
270 0           $self->{path} =~ s:^$root/?::i;
271            
272 0           $self->parse_path;
273 0           $self->parse_args( $request );
274             }
275            
276             sub parse_args
277             {
278 0     0 1   my ( $self, $request ) = @_;
279            
280             # this code stolen from Catalyst::Engine::HTTP::Base::prepare_parameters(),
281             # with **file uploads removed**
282            
283 0           my @params;
284            
285 0           push( @params, $request->uri->query_form );
286            
287 0 0         if ( $request->content_type eq 'application/x-www-form-urlencoded' )
288             {
289 0           my $uri = URI->new('http:');
290 0           $uri->query( $request->content );
291 0           push( @params, $uri->query_form );
292             }
293            
294 0 0         if ( $request->content_type eq 'multipart/form-data' )
295             {
296 0           for my $part ( $request->parts )
297             {
298 0           my $disposition = $part->header('Content-Disposition');
299 0           my %parameters = @{ ( HTTP::Headers::Util::split_header_words($disposition) )[0] };
  0            
300              
301 0 0         die 'File uploads not supported' if $parameters{filename};
302            
303 0           push( @params, $parameters{name}, $part->content );
304             }
305             }
306            
307 0           my %parameters;
308            
309             # this from Catalyst::Request::param()
310 0           while ( my ( $field, $value ) = splice( @params, 0, 2 ) )
311             {
312 0 0         next unless defined $field;
313              
314 0 0         if ( exists $parameters{$field} )
315             {
316 0           for ( $parameters{$field} )
317             {
318 0 0         $_ = [$_] unless ref($_) eq 'ARRAY';
319 0           push( @$_, $value );
320             }
321             }
322             else
323             {
324 0           $parameters{$field} = $value;
325             }
326             }
327            
328             # back to Maypole...
329 0           $self->params( \%parameters );
330 0           $self->query( \%parameters );
331             }
332              
333 0 0   0 1   sub get_template_root { $ENV{MAYPOLE_TEMPLATES} || '.' }
334              
335              
336             1;
337              
338             __END__