File Coverage

blib/lib/Dancer/Test.pm
Criterion Covered Total %
statement 234 254 92.1
branch 46 56 82.1
condition 42 69 60.8
subroutine 45 50 90.0
pod 17 19 89.4
total 384 448 85.7


line stmt bran cond sub pod time code
1             package Dancer::Test;
2             our $AUTHORITY = 'cpan:SUKRIA';
3             #ABSTRACT: Test helpers to test a Dancer application
4             $Dancer::Test::VERSION = '1.3514_04'; # TRIAL
5             $Dancer::Test::VERSION = '1.351404';
6             # test helpers for Dancer apps
7              
8 86     86   874957 use strict;
  86         269  
  86         2176  
9 86     86   392 use warnings;
  86         161  
  86         1870  
10 86     86   4044 use Test::Builder;
  86         335773  
  86         2297  
11 86     86   4403 use Test::More import => [ '!pass' ];
  86         36014  
  86         933  
12 86     86   60104 use Test::LongString;
  86         163153  
  86         484  
13              
14 86     86   6496 use Carp;
  86         178  
  86         3575  
15 86     86   4762 use HTTP::Headers;
  86         57142  
  86         1790  
16 86     86   413 use Scalar::Util 'blessed';
  86         184  
  86         3710  
17              
18 86     86   5876 use Dancer ':syntax', ':tests';
  86         163  
  86         490  
19 86     86   518 use Dancer::App;
  86         180  
  86         2190  
20 86     86   501 use Dancer::Deprecation;
  86         191  
  86         1823  
21 86     86   449 use Dancer::Request;
  86         163  
  86         1877  
22 86     86   401 use Dancer::Request::Upload;
  86         175  
  86         1875  
23 86     86   400 use Dancer::SharedData;
  86         156  
  86         1783  
24 86     86   460 use Dancer::Renderer;
  86         162  
  86         1776  
25 86     86   408 use Dancer::Handler;
  86         174  
  86         2117  
26 86     86   436 use Dancer::Config;
  86         204  
  86         3264  
27 86     86   505 use Dancer::FileUtils qw(open_file);
  86         178  
  86         3895  
28              
29 86     86   501 use base 'Exporter';
  86         194  
  86         10190  
30 86     86   550 use vars '@EXPORT';
  86         183  
  86         209843  
31              
32             @EXPORT = qw(
33             route_exists
34             route_doesnt_exist
35              
36             response_exists
37             response_doesnt_exist
38              
39             response_status_is
40             response_status_isnt
41              
42             response_content_is
43             response_content_isnt
44             response_content_is_deeply
45             response_content_like
46             response_content_unlike
47              
48             response_is_file
49             response_headers_are_deeply
50             response_headers_include
51             response_redirect_location_is
52             response_redirect_location_like
53              
54             dancer_response
55              
56             read_logs
57             );
58              
59             sub import {
60 86     86   676 my ($class, %options) = @_;
61 86   100     646 $options{appdir} ||= '.';
62              
63             # mimic PSGI env
64 86         627 $ENV{SERVERNAME} = 'localhost';
65 86         329 $ENV{HTTP_HOST} = 'localhost';
66 86         330 $ENV{SERVER_PORT} = 80;
67 86         299 $ENV{'psgi.url_scheme'} = 'http';
68              
69 86         372 my ($package, $script) = caller;
70 86         10923 $class->export_to_level(1, $class, @EXPORT);
71              
72 86         511 Dancer::_init_script_dir($options{appdir});
73 86         497 Dancer::Config->load;
74              
75             # set a default session engine for tests
76 86         414 setting 'session' => 'simple';
77              
78             # capture logs for testing
79 86         344 setting 'logger' => 'capture';
80 86         393 setting 'log' => 'debug';
81             }
82              
83             # Route Registry
84              
85             sub _isa {
86 1206     1206   2008 my ( $reference, $classname ) = @_;
87 1206   66     5935 return blessed $reference && $reference->isa($classname);
88             }
89              
90             sub _req_to_response {
91 430     430   1330 my $req = shift;
92              
93             # already a response object
94 430 100       882 return $req if _isa($req, 'Dancer::Response');
95              
96 422 100       1544 return dancer_response( ref $req eq 'ARRAY' ? @$req : ( 'GET', $req ) );
97             }
98              
99             sub _req_label {
100 274     274   486 my $req = shift;
101              
102 274 100       650 return _isa($req, 'Dancer::Response') ? 'response object'
    100          
103             : ref $req eq 'ARRAY' ? join( ' ', @$req )
104             : "GET $req";
105             }
106              
107             sub expand_req {
108 54     54 0 232 my $req = shift;
109 54 50       278 return ref $req eq 'ARRAY' ? @$req : ( 'GET', $req );
110             }
111              
112             sub route_exists {
113 43     43 1 14287 my ($req, $test_name) = @_;
114 43         177 my $tb = Test::Builder->new;
115              
116 43         362 my ($method, $path) = expand_req($req);
117 43   66     284 $test_name ||= "a route exists for $method $path";
118              
119 43         257 $req = Dancer::Request->new_for_request($method => $path);
120 43         206 return $tb->ok(defined(Dancer::App->find_route_through_apps($req)), $test_name);
121             }
122              
123             sub route_doesnt_exist {
124 4     4 1 2641 my ($req, $test_name) = @_;
125 4         16 my $tb = Test::Builder->new;
126              
127 4         29 my ($method, $path) = expand_req($req);
128 4   66     23 $test_name ||= "no route exists for $method $path";
129              
130 4         19 $req = Dancer::Request->new_for_request($method => $path);
131 4         19 return $tb->ok(!defined(Dancer::App->find_route_through_apps($req)), $test_name);
132             }
133              
134             # Response status
135              
136             sub response_exists {
137 0     0 1 0 Dancer::Deprecation->deprecated(
138             fatal => 1,
139             feature => 'response_exists',
140             reason => 'Use response_status_isnt and check for status 404.'
141             );
142             }
143              
144             sub response_doesnt_exist {
145 0     0 1 0 Dancer::Deprecation->deprecated(
146             fatal => 1,
147             feature => 'response_doesnt_exist',
148             reason => 'Use response_status_is and check for status 404.',
149             );
150             }
151              
152             sub response_status_is {
153 160     160 1 79427 my ($req, $status, $test_name) = @_;
154 160   66     792 $test_name ||= "response status is $status for " . _req_label($req);
155              
156 160         378 my $response = _req_to_response($req);
157 160         840 my $tb = Test::Builder->new;
158 160         1192 return $tb->is_eq($response->status, $status, $test_name);
159             }
160              
161             sub response_status_isnt {
162 4     4 1 3203 my ($req, $status, $test_name) = @_;
163 4   66     30 $test_name ||= "response status is not $status for " . _req_label($req);
164              
165 4         14 my $response = _req_to_response($req);
166 4         28 my $tb = Test::Builder->new;
167 4         43 $tb->isnt_eq( $response->{status}, $status, $test_name );
168             }
169              
170             # Response content
171              
172             sub response_content_is {
173 123     123 1 59965 my ($req, $matcher, $test_name) = @_;
174 123   66     551 $test_name ||= "response content looks good for " . _req_label($req);
175              
176 123         547 my $response = _req_to_response($req);
177 123         676 my $tb = Test::Builder->new;
178 123         1014 return $tb->is_eq( $response->{content}, $matcher, $test_name );
179             }
180              
181             sub response_content_isnt {
182 4     4 1 2755 my ($req, $matcher, $test_name) = @_;
183 4   33     30 $test_name ||= "response content looks good for " . _req_label($req);
184              
185 4         15 my $response = _req_to_response($req);
186 4         25 my $tb = Test::Builder->new;
187 4         40 return $tb->isnt_eq( $response->{content}, $matcher, $test_name );
188             }
189              
190             sub response_content_like {
191 41     41 1 16808 my ($req, $matcher, $test_name) = @_;
192 41   66     194 $test_name ||= "response content looks good for " . _req_label($req);
193              
194 41         111 my $response = _req_to_response($req);
195 41         225 return like_string( $response->{content}, $matcher, $test_name ); # better output for long content than Test::Builder
196             }
197              
198             sub response_content_unlike {
199 4     4 1 1748 my ($req, $matcher, $test_name) = @_;
200 4   50     30 $test_name ||= "response content looks good for " , _req_label($req);
201              
202 4         14 my $response = _req_to_response($req);
203 4         23 return unlike_string( $response->{content}, $matcher, $test_name ); # better for long content than Test::Builder
204             }
205              
206             sub response_content_is_deeply {
207 59     59 1 31895 my ($req, $matcher, $test_name) = @_;
208 59   66     216 $test_name ||= "response content looks good for " . _req_label($req);
209              
210 59         110 local $Test::Builder::Level = $Test::Builder::Level + 1;
211 59         96 my $response = _req_to_response($req);
212 59         218 is_deeply $response->{content}, $matcher, $test_name;
213             }
214              
215             sub response_is_file {
216 1     1 0 48 my ($req, $test_name) = @_;
217 1   33     8 $test_name ||= "a file is returned for " . _req_label($req);
218              
219 1         4 my $response = _get_file_response($req);
220 1         5 my $tb = Test::Builder->new;
221 1         8 return $tb->ok(defined($response), $test_name);
222             }
223              
224             sub response_headers_are_deeply {
225 7     7 1 1078 my ($req, $expected, $test_name) = @_;
226 7   66     28 $test_name ||= "headers are as expected for " . _req_label($req);
227              
228 7         14 local $Test::Builder::Level = $Test::Builder::Level + 1;
229 7         16 my $response = _req_to_response($req);
230              
231 7         51 is_deeply(
232             _sort_headers( $response->headers_to_array ),
233             _sort_headers( $expected ),
234             $test_name
235             );
236             }
237              
238             # Sort arrayref of headers (turn it into a list of arrayrefs, sort by the header
239             # & value, then turn it back into an arrayref)
240             sub _sort_headers {
241 14     14   20 my @originalheaders = @{ shift() }; # take a copy we can modify
  14         49  
242 14         35 my @headerpairs;
243 14         38 while (my ($header, $value) = splice @originalheaders, 0, 2) {
244 40         105 push @headerpairs, [ $header, $value ];
245             }
246              
247             # We have an array of arrayrefs holding header => value pairs; sort them by
248             # header then value, and return them flattened back into an arrayref
249             return [
250 40         105 map { @$_ }
251 14 50       34 sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] }
  35         69  
252             @headerpairs
253             ];
254             }
255              
256              
257             sub response_headers_include {
258 27     27 1 8926 my ($req, $expected, $test_name) = @_;
259 27   66     129 $test_name ||= "headers include expected data for " . _req_label($req);
260 27         90 my $tb = Test::Builder->new;
261              
262 27         169 my $response = _req_to_response($req);
263 27         90 return $tb->ok(_include_in_headers($response->headers_to_array, $expected), $test_name);
264             }
265              
266             sub response_redirect_location_is {
267 0     0 1 0 my ($req, $expected, $test_name) = @_;
268 0   0     0 $test_name ||= "redirect location looks good for " . _req_label($req);
269 0         0 my $tb = Test::Builder->new;
270              
271 0         0 my $response = _req_to_response($req);
272 0         0 return $tb->is_eq($response->header('location'), $expected, $test_name);
273             }
274              
275             sub response_redirect_location_like {
276 0     0 1 0 my ($req, $matcher, $test_name) = @_;
277 0   0     0 $test_name ||= "redirect location looks good for " . _req_label($req);
278 0         0 my $tb = Test::Builder->new;
279              
280 0         0 my $response = _req_to_response($req);
281 0         0 return $tb->like($response->header('location'), $matcher, $test_name);
282             }
283              
284              
285             # make sure the given header sublist is included in the full headers array
286             sub _include_in_headers {
287 27     27   58 my ($full_headers, $expected_subset) = @_;
288              
289             # walk through all the expected header pairs, make sure
290             # they exist with the same value in the full_headers list
291             # return false as soon as one is not.
292 27         149 for (my $i=0; $i
293 45         122 my ($name, $value) = ($expected_subset->[$i], $expected_subset->[$i + 1]);
294 45 50       139 return 0
295             unless _check_header($full_headers, $name, $value);
296             }
297              
298             # we've found all the expected pairs in the $full_headers list
299 27         205 return 1;
300             }
301              
302             sub _check_header {
303 45     45   88 my ($headers, $key, $value) = @_;
304 45         108 for (my $i=0; $i
305 112         173 my ($name, $val) = ($headers->[$i], $headers->[$i + 1]);
306 112 100 100     363 return 1 if $name eq $key && $value eq $val;
307             }
308 0         0 return 0;
309             }
310              
311             sub dancer_response {
312 502     502 1 38496 my ($method, $path, $args) = @_;
313 502   100     2103 $args ||= {};
314 502         815 my $extra_env = {};
315              
316 502 100       1655 if ($method =~ /^(?:PUT|POST)$/) {
317              
318 28         63 my ($content, $content_type);
319              
320 28 50 66     212 if ( $args->{body} and $args->{files} ) {
    100          
    100          
321             # XXX: When fixing this, update this method's POD
322 0         0 croak 'dancer_response() does not support both body and files';
323             }
324             elsif ( $args->{body} ) {
325 13         28 $content = $args->{body};
326             $content_type = $args->{content_type}
327 13   100     64 || 'text/plain';
328              
329             # coerce hashref into an url-encoded string
330 13 100 66     50 if ( ref($content) && ( ref($content) eq 'HASH' ) ) {
331 3         6 my @tokens;
332 3         7 while ( my ( $name, $value ) = each %{$content} ) {
  6         26  
333 3         12 $name = _url_encode($name);
334 3 100       16 my @values = ref $value eq 'ARRAY' ? @$value : ($value);
335 3         8 for my $value (@values) {
336 4         9 $value = _url_encode($value);
337 4         18 push @tokens, "${name}=${value}";
338             }
339             }
340 3         24 $content = join( '&', @tokens );
341 3         9 $content_type = 'application/x-www-form-urlencoded';
342             }
343             }
344             elsif ( $args->{files} ) {
345 3         9 $content_type = 'multipart/form-data; boundary=----BOUNDARY';
346 3         6 foreach my $file (@{$args->{files}}){
  3         11  
347 5   50     31 $file->{content_type} ||= 'text/plain';
348 5         11 $content .= qq/------BOUNDARY\r\n/;
349 5         18 $content .= qq/Content-Disposition: form-data; name="$file->{name}"; filename="$file->{filename}"\r\n/;
350 5         13 $content .= qq/Content-Type: $file->{content_type}\r\n\r\n/;
351 5 100       14 if ( $file->{data} ) {
352 1         4 $content .= $file->{data};
353             } else {
354             open my $fh, '<', $file->{filename}
355 4 50       111 or die "Failed to open $file->{filename} - $!";
356 4 50       142 if ( -B $file->{filename} ) {
357 0         0 binmode $fh;
358             }
359 4         62 while (<$fh>) {
360 4         48 $content .= $_;
361             }
362             }
363 5         15 $content .= "\r\n";
364             }
365 3         8 $content .= "------BOUNDARY";
366             }
367              
368 28         54 my $l = 0;
369 28 100       81 $l = length $content if defined $content;
370 28     10   797 open my $in, '<', \$content;
  10         79  
  10         18  
  10         267  
371 28         7917 $extra_env->{'CONTENT_LENGTH'} = $l;
372 28   100     136 $extra_env->{'CONTENT_TYPE'} = $content_type || "";
373 28         73 $extra_env->{'psgi.input'} = $in;
374             }
375              
376 502         1574 my ($params, $body, $headers) = @$args{qw(params body headers)};
377              
378 502 100       1489 $headers = HTTP::Headers->new(@{$headers||[]})
  500 100       2806  
379             unless _isa($headers, "HTTP::Headers");
380              
381 502 100       5078 if ($headers->header('Content-Type')) {
382 16         472 $extra_env->{'CONTENT_TYPE'} = $headers->header('Content-Type');
383             }
384              
385             # handle all the keys of Request::_build_request_env():
386 502         17876 for my $key (qw( user_agent host accept_language accept_charset
387             accept_encoding keep_alive connection accept accept_type referer
388             x_requested_with )) {
389 5522         9232 my $k = sprintf("HTTP_%s", uc $key);
390             $extra_env->{$k} = $headers->{$key}
391 5522 100       9151 if exists $headers->{$key};
392             }
393              
394             # fake the REQUEST_URI
395             # TODO deal with the params
396 502 50       1791 unless( $extra_env->{REQUEST_URI} ) {
397 502         1284 $extra_env->{REQUEST_URI} = $path;
398 502 100 100     2152 if ( $method eq 'GET' and $params ) {
399             $extra_env->{REQUEST_URI} .=
400 8         41 '?' . join '&', map { join '=', $_, $params->{$_} }
  7         41  
401             sort keys %$params;
402             }
403             }
404              
405 502         2409 my $request = Dancer::Request->new_for_request(
406             $method => $path,
407             $params, $body, $headers, $extra_env
408             );
409              
410             # first, reset the current state
411 502         1894 Dancer::SharedData->reset_all();
412              
413             # then store the request
414 502         1508 Dancer::SharedData->request($request);
415              
416             # XXX this is a hack!!
417 502 100       1625 $request = Dancer::Serializer->process_request($request)
418             if Dancer::App->current->setting('serializer');
419              
420 502         1687 my $get_action = Dancer::Handler::render_request($request);
421 502         1445 my $response = Dancer::SharedData->response();
422              
423 502 100       1252 $response->content('') if $method eq 'HEAD';
424 502         1312 Dancer::SharedData->reset_response();
425 502 50       2451 return $response if $get_action;
426 0 0 0     0 (defined $response && $response->exists) ? return $response : return undef;
427             }
428              
429             # private
430              
431             sub _url_encode {
432 7     7   11 my $string = shift;
433 7         19 $string =~ s/([\W])/"%" . uc(sprintf("%2.2x",ord($1)))/eg;
  1         9  
434 7         12 return $string;
435             }
436              
437             sub _get_file_response {
438 7     7   1344 my ($req) = @_;
439              
440 7         15 my ($method, $path, $params) = expand_req($req);
441 7         34 my $request = Dancer::Request->new_for_request($method => $path, $params);
442 7         22 Dancer::SharedData->request($request);
443 7         98 return Dancer::Renderer::get_file_response();
444             }
445              
446             sub _get_handler_response {
447 0     0   0 my ($req) = @_;
448 0         0 my ($method, $path, $params) = expand_req($req);
449 0         0 my $request = Dancer::Request->new_for_request($method => $path, $params);
450 0         0 return Dancer::Handler->handle_request($request);
451             }
452              
453             sub read_logs {
454 2     2 1 10 return Dancer::Logger::Capture->trap->read;
455             }
456              
457              
458             1;
459              
460             __END__