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.3521';
5             # test helpers for Dancer apps
6              
7 85     85   1058373 use strict;
  85         324  
  85         2625  
8 85     85   523 use warnings;
  85         244  
  85         2284  
9 85     85   5076 use Test::Builder;
  85         398408  
  85         2837  
10 85     85   4874 use Test::More import => [ '!pass' ];
  85         43180  
  85         1363  
11 85     85   74578 use Test::LongString;
  85         195007  
  85         639  
12              
13 85     85   7011 use Carp;
  85         240  
  85         4162  
14 85     85   5900 use HTTP::Headers;
  85         68322  
  85         2013  
15 85     85   503 use Scalar::Util 'blessed';
  85         205  
  85         4365  
16              
17 85     85   7705 use Dancer ':syntax', ':tests';
  85         229  
  85         680  
18 85     85   641 use Dancer::App;
  85         280  
  85         2396  
19 85     85   544 use Dancer::Deprecation;
  85         225  
  85         2303  
20 85     85   520 use Dancer::Request;
  85         247  
  85         2192  
21 85     85   526 use Dancer::Request::Upload;
  85         213  
  85         2342  
22 85     85   501 use Dancer::SharedData;
  85         208  
  85         2103  
23 85     85   533 use Dancer::Renderer;
  85         252  
  85         2377  
24 85     85   519 use Dancer::Handler;
  85         247  
  85         2398  
25 85     85   546 use Dancer::Config;
  85         228  
  85         4046  
26 85     85   596 use Dancer::FileUtils qw(open_file);
  85         222  
  85         4422  
27              
28 85     85   617 use base 'Exporter';
  85         266  
  85         11513  
29 85     85   626 use vars '@EXPORT';
  85         281  
  85         254408  
30              
31             @EXPORT = qw(
32             route_exists
33             route_doesnt_exist
34              
35             response_exists
36             response_doesnt_exist
37              
38             response_status_is
39             response_status_isnt
40              
41             response_content_is
42             response_content_isnt
43             response_content_is_deeply
44             response_content_like
45             response_content_unlike
46              
47             response_is_file
48             response_headers_are_deeply
49             response_headers_include
50             response_redirect_location_is
51             response_redirect_location_like
52              
53             dancer_response
54              
55             read_logs
56             );
57              
58             sub import {
59 85     85   770 my ($class, %options) = @_;
60 85   100     880 $options{appdir} ||= '.';
61              
62             # mimic PSGI env
63 85         856 $ENV{SERVERNAME} = 'localhost';
64 85         393 $ENV{HTTP_HOST} = 'localhost';
65 85         405 $ENV{SERVER_PORT} = 80;
66 85         395 $ENV{'psgi.url_scheme'} = 'http';
67              
68 85         358 my ($package, $script) = caller;
69 85         12344 $class->export_to_level(1, $class, @EXPORT);
70              
71 85         599 Dancer::_init_script_dir($options{appdir});
72 85         647 Dancer::Config->load;
73              
74             # set a default session engine for tests
75 85         508 setting 'session' => 'simple';
76              
77             # capture logs for testing
78 85         352 setting 'logger' => 'capture';
79 85         351 setting 'log' => 'debug';
80             }
81              
82             # Route Registry
83              
84             sub _isa {
85 1200     1200   2131 my ( $reference, $classname ) = @_;
86 1200   66     6142 return blessed $reference && $reference->isa($classname);
87             }
88              
89             sub _req_to_response {
90 427     427   1366 my $req = shift;
91              
92             # already a response object
93 427 100       832 return $req if _isa($req, 'Dancer::Response');
94              
95 419 100       1796 return dancer_response( ref $req eq 'ARRAY' ? @$req : ( 'GET', $req ) );
96             }
97              
98             sub _req_label {
99 274     274   541 my $req = shift;
100              
101 274 100       682 return _isa($req, 'Dancer::Response') ? 'response object'
    100          
102             : ref $req eq 'ARRAY' ? join( ' ', @$req )
103             : "GET $req";
104             }
105              
106             sub expand_req {
107 54     54 0 134 my $req = shift;
108 54 50       289 return ref $req eq 'ARRAY' ? @$req : ( 'GET', $req );
109             }
110              
111             sub route_exists {
112 43     43 1 10852 my ($req, $test_name) = @_;
113 43         217 my $tb = Test::Builder->new;
114              
115 43         423 my ($method, $path) = expand_req($req);
116 43   66     299 $test_name ||= "a route exists for $method $path";
117              
118 43         299 $req = Dancer::Request->new_for_request($method => $path);
119 43         221 return $tb->ok(defined(Dancer::App->find_route_through_apps($req)), $test_name);
120             }
121              
122             sub route_doesnt_exist {
123 4     4 1 1846 my ($req, $test_name) = @_;
124 4         18 my $tb = Test::Builder->new;
125              
126 4         34 my ($method, $path) = expand_req($req);
127 4   66     33 $test_name ||= "no route exists for $method $path";
128              
129 4         17 $req = Dancer::Request->new_for_request($method => $path);
130 4         32 return $tb->ok(!defined(Dancer::App->find_route_through_apps($req)), $test_name);
131             }
132              
133             # Response status
134              
135             sub response_exists {
136 0     0 1 0 Dancer::Deprecation->deprecated(
137             fatal => 1,
138             feature => 'response_exists',
139             reason => 'Use response_status_isnt and check for status 404.'
140             );
141             }
142              
143             sub response_doesnt_exist {
144 0     0 1 0 Dancer::Deprecation->deprecated(
145             fatal => 1,
146             feature => 'response_doesnt_exist',
147             reason => 'Use response_status_is and check for status 404.',
148             );
149             }
150              
151             sub response_status_is {
152 160     160 1 78500 my ($req, $status, $test_name) = @_;
153 160   66     787 $test_name ||= "response status is $status for " . _req_label($req);
154              
155 160         413 my $response = _req_to_response($req);
156 160         820 my $tb = Test::Builder->new;
157 160         1278 return $tb->is_eq($response->status, $status, $test_name);
158             }
159              
160             sub response_status_isnt {
161 4     4 1 2499 my ($req, $status, $test_name) = @_;
162 4   66     22 $test_name ||= "response status is not $status for " . _req_label($req);
163              
164 4         9 my $response = _req_to_response($req);
165 4         19 my $tb = Test::Builder->new;
166 4         35 $tb->isnt_eq( $response->{status}, $status, $test_name );
167             }
168              
169             # Response content
170              
171             sub response_content_is {
172 121     121 1 59449 my ($req, $matcher, $test_name) = @_;
173 121   66     576 $test_name ||= "response content looks good for " . _req_label($req);
174              
175 121         320 my $response = _req_to_response($req);
176 121         722 my $tb = Test::Builder->new;
177 121         1038 return $tb->is_eq( $response->{content}, $matcher, $test_name );
178             }
179              
180             sub response_content_isnt {
181 4     4 1 2316 my ($req, $matcher, $test_name) = @_;
182 4   33     37 $test_name ||= "response content looks good for " . _req_label($req);
183              
184 4         14 my $response = _req_to_response($req);
185 4         18 my $tb = Test::Builder->new;
186 4         35 return $tb->isnt_eq( $response->{content}, $matcher, $test_name );
187             }
188              
189             sub response_content_like {
190 40     40 1 14929 my ($req, $matcher, $test_name) = @_;
191 40   66     194 $test_name ||= "response content looks good for " . _req_label($req);
192              
193 40         125 my $response = _req_to_response($req);
194 40         274 return like_string( $response->{content}, $matcher, $test_name ); # better output for long content than Test::Builder
195             }
196              
197             sub response_content_unlike {
198 4     4 1 1530 my ($req, $matcher, $test_name) = @_;
199 4   50     27 $test_name ||= "response content looks good for " , _req_label($req);
200              
201 4         12 my $response = _req_to_response($req);
202 4         26 return unlike_string( $response->{content}, $matcher, $test_name ); # better for long content than Test::Builder
203             }
204              
205             sub response_content_is_deeply {
206 59     59 1 35013 my ($req, $matcher, $test_name) = @_;
207 59   66     244 $test_name ||= "response content looks good for " . _req_label($req);
208              
209 59         131 local $Test::Builder::Level = $Test::Builder::Level + 1;
210 59         126 my $response = _req_to_response($req);
211 59         273 is_deeply $response->{content}, $matcher, $test_name;
212             }
213              
214             sub response_is_file {
215 1     1 0 67 my ($req, $test_name) = @_;
216 1   33     9 $test_name ||= "a file is returned for " . _req_label($req);
217              
218 1         5 my $response = _get_file_response($req);
219 1         12 my $tb = Test::Builder->new;
220 1         11 return $tb->ok(defined($response), $test_name);
221             }
222              
223             sub response_headers_are_deeply {
224 7     7 1 1531 my ($req, $expected, $test_name) = @_;
225 7   66     30 $test_name ||= "headers are as expected for " . _req_label($req);
226              
227 7         18 local $Test::Builder::Level = $Test::Builder::Level + 1;
228 7         21 my $response = _req_to_response($req);
229              
230 7         75 is_deeply(
231             _sort_headers( $response->headers_to_array ),
232             _sort_headers( $expected ),
233             $test_name
234             );
235             }
236              
237             # Sort arrayref of headers (turn it into a list of arrayrefs, sort by the header
238             # & value, then turn it back into an arrayref)
239             sub _sort_headers {
240 14     14   21 my @originalheaders = @{ shift() }; # take a copy we can modify
  14         39  
241 14         20 my @headerpairs;
242 14         50 while (my ($header, $value) = splice @originalheaders, 0, 2) {
243 40         123 push @headerpairs, [ $header, $value ];
244             }
245              
246             # We have an array of arrayrefs holding header => value pairs; sort them by
247             # header then value, and return them flattened back into an arrayref
248             return [
249 40         147 map { @$_ }
250 14 50       42 sort { $a->[0] cmp $b->[0] || $a->[1] cmp $b->[1] }
  35         86  
251             @headerpairs
252             ];
253             }
254              
255              
256             sub response_headers_include {
257 27     27 1 10598 my ($req, $expected, $test_name) = @_;
258 27   66     132 $test_name ||= "headers include expected data for " . _req_label($req);
259 27         119 my $tb = Test::Builder->new;
260              
261 27         215 my $response = _req_to_response($req);
262 27         140 return $tb->ok(_include_in_headers($response->headers_to_array, $expected), $test_name);
263             }
264              
265             sub response_redirect_location_is {
266 0     0 1 0 my ($req, $expected, $test_name) = @_;
267 0   0     0 $test_name ||= "redirect location looks good for " . _req_label($req);
268 0         0 my $tb = Test::Builder->new;
269              
270 0         0 my $response = _req_to_response($req);
271 0         0 return $tb->is_eq($response->header('location'), $expected, $test_name);
272             }
273              
274             sub response_redirect_location_like {
275 0     0 1 0 my ($req, $matcher, $test_name) = @_;
276 0   0     0 $test_name ||= "redirect location looks good for " . _req_label($req);
277 0         0 my $tb = Test::Builder->new;
278              
279 0         0 my $response = _req_to_response($req);
280 0         0 return $tb->like($response->header('location'), $matcher, $test_name);
281             }
282              
283              
284             # make sure the given header sublist is included in the full headers array
285             sub _include_in_headers {
286 27     27   103 my ($full_headers, $expected_subset) = @_;
287              
288             # walk through all the expected header pairs, make sure
289             # they exist with the same value in the full_headers list
290             # return false as soon as one is not.
291 27         168 for (my $i=0; $i
292 45         150 my ($name, $value) = ($expected_subset->[$i], $expected_subset->[$i + 1]);
293 45 50       103 return 0
294             unless _check_header($full_headers, $name, $value);
295             }
296              
297             # we've found all the expected pairs in the $full_headers list
298 27         276 return 1;
299             }
300              
301             sub _check_header {
302 45     45   110 my ($headers, $key, $value) = @_;
303 45         137 for (my $i=0; $i
304 112         209 my ($name, $val) = ($headers->[$i], $headers->[$i + 1]);
305 112 100 100     491 return 1 if $name eq $key && $value eq $val;
306             }
307 0         0 return 0;
308             }
309              
310             sub dancer_response {
311 499     499 1 38892 my ($method, $path, $args) = @_;
312 499   100     2177 $args ||= {};
313 499         963 my $extra_env = {};
314              
315 499 100       1734 if ($method =~ /^(?:PUT|POST)$/) {
316              
317 28         62 my ($content, $content_type);
318              
319 28 50 66     224 if ( $args->{body} and $args->{files} ) {
    100          
    100          
320             # XXX: When fixing this, update this method's POD
321 0         0 croak 'dancer_response() does not support both body and files';
322             }
323             elsif ( $args->{body} ) {
324 13         29 $content = $args->{body};
325             $content_type = $args->{content_type}
326 13   100     53 || 'text/plain';
327              
328             # coerce hashref into an url-encoded string
329 13 100 66     53 if ( ref($content) && ( ref($content) eq 'HASH' ) ) {
330 3         7 my @tokens;
331 3         6 while ( my ( $name, $value ) = each %{$content} ) {
  6         27  
332 3         13 $name = _url_encode($name);
333 3 100       18 my @values = ref $value eq 'ARRAY' ? @$value : ($value);
334 3         9 for my $value (@values) {
335 4         17 $value = _url_encode($value);
336 4         18 push @tokens, "${name}=${value}";
337             }
338             }
339 3         12 $content = join( '&', @tokens );
340 3         9 $content_type = 'application/x-www-form-urlencoded';
341             }
342             }
343             elsif ( $args->{files} ) {
344 3         10 $content_type = 'multipart/form-data; boundary=----BOUNDARY';
345 3         6 foreach my $file (@{$args->{files}}){
  3         12  
346 5   50     28 $file->{content_type} ||= 'text/plain';
347 5         16 $content .= qq/------BOUNDARY\r\n/;
348 5         16 $content .= qq/Content-Disposition: form-data; name="$file->{name}"; filename="$file->{filename}"\r\n/;
349 5         16 $content .= qq/Content-Type: $file->{content_type}\r\n\r\n/;
350 5 100       11 if ( $file->{data} ) {
351 1         4 $content .= $file->{data};
352             } else {
353             open my $fh, '<', $file->{filename}
354 4 50       125 or die "Failed to open $file->{filename} - $!";
355 4 50       153 if ( -B $file->{filename} ) {
356 0         0 binmode $fh;
357             }
358 4         63 while (<$fh>) {
359 4         52 $content .= $_;
360             }
361             }
362 5         18 $content .= "\r\n";
363             }
364 3         7 $content .= "------BOUNDARY";
365             }
366              
367 28         52 my $l = 0;
368 28 100       82 $l = length $content if defined $content;
369 28     10   749 open my $in, '<', \$content;
  10         90  
  10         27  
  10         81  
370 28         8764 $extra_env->{'CONTENT_LENGTH'} = $l;
371 28   100     131 $extra_env->{'CONTENT_TYPE'} = $content_type || "";
372 28         76 $extra_env->{'psgi.input'} = $in;
373             }
374              
375 499         1481 my ($params, $body, $headers) = @$args{qw(params body headers)};
376              
377 499 100       1056 $headers = HTTP::Headers->new(@{$headers||[]})
  497 100       3276  
378             unless _isa($headers, "HTTP::Headers");
379              
380 499 100       5918 if ($headers->header('Content-Type')) {
381 16         549 $extra_env->{'CONTENT_TYPE'} = $headers->header('Content-Type');
382             }
383              
384             # handle all the keys of Request::_build_request_env():
385 499         20928 for my $key (qw( user_agent host accept_language accept_charset
386             accept_encoding keep_alive connection accept accept_type referer
387             x_requested_with )) {
388 5489         11002 my $k = sprintf("HTTP_%s", uc $key);
389             $extra_env->{$k} = $headers->{$key}
390 5489 100       10689 if exists $headers->{$key};
391             }
392              
393             # fake the REQUEST_URI
394             # TODO deal with the params
395 499 50       1491 unless( $extra_env->{REQUEST_URI} ) {
396 499         1182 $extra_env->{REQUEST_URI} = $path;
397 499 100 100     2116 if ( $method eq 'GET' and $params ) {
398             $extra_env->{REQUEST_URI} .=
399 8         52 '?' . join '&', map { join '=', $_, $params->{$_} }
  7         44  
400             sort keys %$params;
401             }
402             }
403              
404 499         2477 my $request = Dancer::Request->new_for_request(
405             $method => $path,
406             $params, $body, $headers, $extra_env
407             );
408              
409             # first, reset the current state
410 499         2045 Dancer::SharedData->reset_all();
411              
412             # then store the request
413 499         1526 Dancer::SharedData->request($request);
414              
415             # XXX this is a hack!!
416 499 100       1589 $request = Dancer::Serializer->process_request($request)
417             if Dancer::App->current->setting('serializer');
418              
419 499         1738 my $get_action = Dancer::Handler::render_request($request);
420 499         1631 my $response = Dancer::SharedData->response();
421              
422 499 100       1393 $response->content('') if $method eq 'HEAD';
423 499         1348 Dancer::SharedData->reset_response();
424 499 50       2591 return $response if $get_action;
425 0 0 0     0 (defined $response && $response->exists) ? return $response : return undef;
426             }
427              
428             # private
429              
430             sub _url_encode {
431 7     7   15 my $string = shift;
432 7         18 $string =~ s/([\W])/"%" . uc(sprintf("%2.2x",ord($1)))/eg;
  1         10  
433 7         17 return $string;
434             }
435              
436             sub _get_file_response {
437 7     7   2058 my ($req) = @_;
438              
439 7         19 my ($method, $path, $params) = expand_req($req);
440 7         58 my $request = Dancer::Request->new_for_request($method => $path, $params);
441 7         33 Dancer::SharedData->request($request);
442 7         147 return Dancer::Renderer::get_file_response();
443             }
444              
445             sub _get_handler_response {
446 0     0   0 my ($req) = @_;
447 0         0 my ($method, $path, $params) = expand_req($req);
448 0         0 my $request = Dancer::Request->new_for_request($method => $path, $params);
449 0         0 return Dancer::Handler->handle_request($request);
450             }
451              
452             sub read_logs {
453 2     2 1 10 return Dancer::Logger::Capture->trap->read;
454             }
455              
456              
457             1;
458              
459             __END__