File Coverage

blib/lib/Test2/Tools/HTTP.pm
Criterion Covered Total %
statement 228 247 92.3
branch 51 62 82.2
condition 16 24 66.6
subroutine 54 70 77.1
pod 31 31 100.0
total 380 434 87.5


line stmt bran cond sub pod time code
1             package Test2::Tools::HTTP;
2              
3 6     6   853709 use strict;
  6         15  
  6         296  
4 6     6   40 use warnings;
  6         68  
  6         389  
5 6     6   151 use 5.014;
  6         29  
6 6     6   4702 use LWP::UserAgent;
  6         378983  
  6         368  
7 6     6   69 use parent qw( Exporter );
  6         14  
  6         59  
8 6     6   525 use Test2::API qw( context );
  6         14  
  6         423  
9 6     6   45 use Test2::Compare;
  6         15  
  6         290  
10 6     6   48 use Test2::Compare::Wildcard;
  6         16  
  6         270  
11 6     6   36 use Test2::Compare::Custom;
  6         13  
  6         165  
12 6     6   4080 use Test2::Tools::HTTP::UA;
  6         30  
  6         231  
13 6     6   40 use Test2::Tools::HTTP::Apps;
  6         12  
  6         124  
14 6     6   2896 use Test2::Tools::HTTP::Tx;
  6         19  
  6         224  
15 6     6   35 use URI;
  6         13  
  6         119  
16 6     6   30 use Carp ();
  6         12  
  6         1547  
17              
18             our %EXPORT_TAGS = (
19             short => [qw(
20             app_add app_del app_guard req ua res code message content content_type charset content_length content_length_ok location location_uri tx headers header
21             )],
22             );
23              
24             our @EXPORT = qw(
25             http_request http_ua http_base_url psgi_app_add psgi_app_del http_response http_code http_message http_content http_tx http_is_success
26             http_is_info http_is_success http_is_redirect http_is_error http_is_client_error http_is_server_error
27             http_isnt_info http_isnt_success http_isnt_redirect http_isnt_error http_isnt_client_error http_isnt_server_error
28             http_content_type http_content_type_charset http_content_length http_content_length_ok http_location http_location_uri
29             http_headers http_header
30             psgi_app_guard
31             );
32              
33             our @EXPORT_OK = (
34             @{ $EXPORT_TAGS{'short'} },
35             );
36              
37             *ua = \&http_ua;
38             *req = \&http_request;
39             *res = \&http_response;
40             *app_add = \&psgi_app_add;
41             *app_del = \&psgi_app_del;
42             *app_guard = \&psgi_app_guard;
43             *charset = \&http_content_type_charset;
44              
45             foreach my $short (qw( code message content content_type content_length content_length_ok location location_uri tx header headers ))
46             {
47 6     6   48 no strict 'refs';
  6         12  
  6         30981  
48             *{$short} = \&{"http_$short"};
49             }
50              
51             # ABSTRACT: Test HTTP / PSGI
52             our $VERSION = '0.12'; # VERSION
53              
54              
55             my $tx;
56             my $apps = Test2::Tools::HTTP::UA->apps;
57             my $ua_wrapper;
58              
59             sub http_request
60             {
61 35     35 1 10977 my($req, $check, $message) = @_;
62              
63 35         76 my %options;
64              
65 35 100       147 if(ref $req eq 'ARRAY')
66             {
67 1         4 ($req, %options) = @$req;
68             }
69              
70 35         174 $req = $req->clone;
71              
72 35         5496 my $url = URI->new_abs($req->uri, http_base_url());
73              
74 35   33     6773 $message ||= "@{[ $req->method ]} @{[ $url ]}";
  35         158  
  35         569  
75              
76 35         436 my $ctx = context();
77 35         15835 my $ok = 1;
78 35         74 my @diag;
79 35         70 my $connection_error = 0;
80              
81 35 100       154 unless($apps->uri_to_app($req->uri))
82             {
83 3 100       15 if($req->uri =~ /^\//)
84             {
85 1         22 $req->uri(
86             URI->new_abs($req->uri, $apps->base_url),
87             );
88             }
89             }
90              
91 35         586 http_ua(); # sets $ua_wrapper if not already
92 35         68 my $res = eval { $ua_wrapper->request($req, %options) };
  35         222  
93              
94 35 100       156 if(my $error = $@)
95             {
96 1         3 $ok = 0;
97 1         4 $connection_error = "$error";
98 1         3 push @diag, "$error";
99 1         3 $res = eval { $error->res };
  1         4  
100             }
101              
102 35 50       149 if(defined $res)
103             {
104 35         260 bless($res, 'Test2::Tools::HTTP::Tx::Response'),
105             }
106              
107 35 100 100     187 if($ok && defined $check)
108             {
109 31         186 my $delta = Test2::Compare::compare($res, $check, \&Test2::Compare::strict_convert);
110 31 100       723 if($delta)
111             {
112 9         21 $ok = 0;
113 9         49 push @diag, $delta->diag->as_string;
114             }
115             }
116              
117 35         72278 $ctx->ok($ok, $message, \@diag);
118 35         12906 $ctx->release;
119              
120             $tx = bless {
121             req => bless($req, 'Test2::Tools::HTTP::Tx::Request'),
122             res => $res,
123             ok => $ok,
124             connection_error => $connection_error,
125 35         1789 location => do {
126 35 100       250 $res
    50          
127             ? $res->header('Location')
128             ? URI->new_abs($res->header('Location'), $res->base)
129             : undef
130             : undef;
131             },
132             }, 'Test2::Tools::HTTP::Tx';
133              
134 35         7453 $ok;
135             }
136              
137              
138             sub http_response (&)
139             {
140 40     40 1 33705 Test2::Compare::build(
141             'Test2::Tools::HTTP::ResponseCompare',
142             @_,
143             );
144             }
145              
146              
147             sub _caller
148             {
149 70     70   132 my $i = 1;
150 70         101 my @caller;
151 70         491 while(@caller = caller $i)
152             {
153 155 100       405 last if $caller[0] ne __PACKAGE__;
154 85         450 $i++;
155             }
156 70         257 @caller;
157             }
158              
159             sub _build
160             {
161 72 100   72   195 defined(my $build = Test2::Compare::get_build()) or Carp::croak "No current build!";
162 71 100       741 Carp::croak "'$build' is not a Test2::Tools::HTTP::ResponseCompare"
163             unless $build->isa('Test2::Tools::HTTP::ResponseCompare');
164              
165 70         182 my @caller = _caller;
166              
167 70         132 my $func_name = $caller[3];
168 70         444 $func_name =~ s/^.*:://;
169 70 100       340 Carp::croak "'$func_name' should only ever be called in void context"
170             if defined $caller[5];
171              
172 69         303 ($build, file => $caller[1], lines => [$caller[2]]);
173             }
174              
175             sub _add_call
176             {
177 17     17   53 my($name, $expect, $context) = @_;
178 17   50     97 $context ||= 'scalar';
179 17         48 my($build, @cmpargs) = _build;
180 14         99 $build->add_call(
181             $name,
182             Test2::Compare::Wildcard->new(
183             expect => $expect,
184             @cmpargs,
185             ),
186             undef,
187             $context
188             );
189             }
190              
191             sub http_code ($)
192             {
193 12     12 1 2872 my($expect) = @_;
194 12         39 _add_call('code', $expect);
195             }
196              
197              
198             sub http_message ($)
199             {
200 4     4 1 260 my($expect) = @_;
201 4         20 _add_call('message', $expect);
202             }
203              
204              
205             sub http_content ($)
206             {
207 17     17 1 658 my($expect) = @_;
208 17         46 my($build, @cmpargs) = _build;
209             $build->add_http_check(
210             sub {
211 17     17   37 my($res) = @_;
212 17   33     136 ($res->decoded_content || $res->content, 1);
213             },
214 17         234 [DREF => 'content'],
215             Test2::Compare::Wildcard->new(
216             expect => $expect,
217             @cmpargs,
218             )
219             );
220             }
221              
222              
223             sub _T()
224             {
225 0     0   0 my @caller = _caller;
226             Test2::Compare::Custom->new(
227 0 0   0   0 code => sub { $_ ? 1 : 0 },
228 0         0 name => 'TRUE',
229             operator => 'TRUE()',
230             file => $caller[1],
231             lines => [$caller[2]],
232             );
233             }
234              
235 0     0 1 0 sub http_is_info { _add_call('is_info', _T()) }
236 0     0 1 0 sub http_is_success { _add_call('is_success', _T()) }
237 0     0 1 0 sub http_is_redirect { _add_call('is_redirect', _T()) }
238 0     0 1 0 sub http_is_error { _add_call('is_error', _T()) }
239 0     0 1 0 sub http_is_client_error { _add_call('is_client_error', _T()) }
240 0     0 1 0 sub http_is_server_error { _add_call('is_server_error', _T()) }
241              
242              
243             sub _F()
244             {
245 0     0   0 my @caller = _caller;
246             Test2::Compare::Custom->new(
247 0 0   0   0 code => sub { $_ ? 0 : 1 },
248 0         0 name => 'TRUE',
249             operator => 'TRUE()',
250             file => $caller[1],
251             lines => [$caller[2]],
252             );
253             }
254              
255 0     0 1 0 sub http_isnt_info { _add_call('is_info', _F()) }
256 0     0 1 0 sub http_isnt_success { _add_call('is_success', _F()) }
257 0     0 1 0 sub http_isnt_redirect { _add_call('is_redirect', _F()) }
258 0     0 1 0 sub http_isnt_error { _add_call('is_error', _F()) }
259 0     0 1 0 sub http_isnt_client_error { _add_call('is_client_error', _F()) }
260 0     0 1 0 sub http_isnt_server_error { _add_call('is_server_error', _F()) }
261              
262              
263             sub http_headers
264             {
265 3     3 1 1049 my($expect) = @_;
266 3         11 my($build, @cmpargs) = _build;
267             $build->add_http_check(
268             sub {
269 3     3   7 my($res) = @_;
270              
271 3         61 my @headers = $res->flatten;
272 3         775 my %headers;
273 3         12 while(@headers)
274             {
275 20         31 my($key, $val) = splice @headers, 0, 2;
276 20         35 push @{ $headers{$key} }, $val;
  20         46  
277             }
278 3         10 $_ = join ',', @{$_} for values %headers;
  16         33  
279              
280 3         10 (\%headers, 1);
281             },
282 3         26 [DREF => 'headers'],
283             Test2::Compare::Wildcard->new(
284             expect => $expect,
285             @cmpargs,
286             ),
287             );
288             }
289              
290              
291             sub http_header
292             {
293 11     11 1 575 my($name, $expect) = @_;
294 11         19 my($build, @cmpargs) = _build;
295             $build->add_http_check(
296             sub {
297 11     11   18 my($res) = @_;
298 11         32 my @values = $res->header($name);
299 11 100       485 return (0,0) unless @values;
300 10 100 100     27 if(ref($expect) eq 'ARRAY' || eval { $expect->isa('Test2::Compare::Array') })
  8         71  
301             {
302 4         6 return ([map { split /,/, $_ } @values], 1);
  8         31  
303             }
304             else
305             {
306 6         25 return (join(',',@values),1);
307             }
308             },
309 11         70 [DREF => "header $name"],
310             Test2::Compare::Wildcard->new(
311             expect => $expect,
312             @cmpargs,
313             ),
314             );
315             }
316              
317              
318             sub http_content_type
319             {
320 6     6 1 162 my($expect) = @_;
321 6         17 my($build, @cmpargs) = _build;
322             $build->add_http_check(
323             sub {
324 6     6   13 my($res) = @_;
325 6         21 my $content_type = $res->content_type;
326 6 50       198 defined $content_type
327             ? ($content_type, 1)
328             : ($content_type, 0);
329             },
330 6         56 [DREF => 'header content-type'],
331             Test2::Compare::Wildcard->new(
332             expect => $expect,
333             @cmpargs,
334             )
335             );
336             }
337              
338             sub http_content_type_charset
339             {
340 6     6 1 31 my($expect) = @_;
341 6         16 my($build, @cmpargs) = _build;
342             $build->add_http_check(
343             sub {
344 6     6   12 my($res) = @_;
345 6         23 my $charset = $res->content_type_charset;
346 6 50       726 defined $charset
347             ? ($charset, 1)
348             : ($charset, 0);
349             },
350 6         57 [DREF => 'header content-type charset'],
351             Test2::Compare::Wildcard->new(
352             expect => $expect,
353             @cmpargs,
354             )
355             );
356             }
357              
358             # TODO: header $key => $check
359             # TODO: cookie $key => $check ??
360              
361              
362             sub http_content_length
363             {
364 1     1 1 10 my($check) = @_;
365 1         4 _add_call('content_length', $check);
366             }
367              
368              
369             sub http_content_length_ok
370             {
371 3     3 1 62 my($build, @cmpargs) = _build;
372              
373             $build->add_http_check(
374             sub {
375 3     3   7 my($res) = @_;
376              
377             (
378 3         22 $res->content_length,
379             1,
380             Test2::Compare::Wildcard->new(
381             expect => length($res->content),
382             @cmpargs,
383             ),
384             )
385             },
386 3         48 [METHOD => 'content_length'],
387             undef,
388             );
389              
390              
391             }
392              
393              
394             sub http_location
395             {
396 5     5 1 233 my($expect) = @_;
397 5         49 my($build, @cmpargs) = _build;
398             $build->add_http_check(
399             sub {
400 5     5   14 my($res) = @_;
401 5         18 my $location = $res->header('Location');
402             (
403 5         323 $location,
404             defined $location
405             )
406             },
407 5         56 [DEREF => "header('Location')"],
408             Test2::Compare::Wildcard->new(
409             expect => $expect,
410             @cmpargs,
411             ),
412             );
413             }
414              
415             sub http_location_uri
416             {
417 4     4 1 482 my($expect) = @_;
418 4         15 my($build, @cmpargs) = _build;
419             $build->add_http_check(
420             sub {
421 4     4   7 my($res) = @_;
422 4         15 my $location = $res->header('Location');
423 4 100       187 defined $location
424             ? (URI->new_abs($location, $res->base), 1)
425             : (undef, 0);
426             },
427 4         39 [DEREF => "header('Location')"],
428             Test2::Compare::Wildcard->new(
429             expect => $expect,
430             @cmpargs,
431             ),
432             );
433             }
434              
435              
436             sub http_tx
437             {
438 36     36 1 38518 $tx;
439             }
440              
441              
442             sub http_base_url
443             {
444 52     52 1 30361 my($new) = @_;
445 52         264 $apps->base_url($new);
446             }
447              
448              
449             sub http_ua
450             {
451 37     37 1 1053696 my($new) = @_;
452              
453 37 50 66     146 if( (!defined $ua_wrapper) && !$new)
454             {
455 4         86 $new = LWP::UserAgent->new;
456 4         16459 $new->env_proxy;
457 4         22834 $new->cookie_jar({});
458             }
459              
460 37 100       48303 if($new)
461             {
462 4         68 $ua_wrapper = Test2::Tools::HTTP::UA->new($new);
463 4         42 $ua_wrapper->instrument;
464             }
465              
466 37         582 $ua_wrapper->ua;
467             }
468              
469              
470             sub psgi_app_add
471             {
472 13 100   13 1 593883 my($url, $app) = @_ == 1 ? (http_base_url, @_) : (@_);
473 13         68 $apps->add_psgi($url, $app);
474 13         45 return;
475             }
476              
477              
478             sub psgi_app_del
479             {
480 8     8 1 3412 my($url) = @_;
481 8   66     121 $url ||= http_base_url;
482 8         54 $apps->del_psgi($url);
483 8         28 return;
484             }
485              
486              
487             sub psgi_app_guard
488             {
489 4 100   4 1 5750 my(%h) = @_ == 1 ? (http_base_url, @_) : (@_);
490              
491 4 100       202 Carp::croak "psgi_app_guard called in void context" unless defined wantarray; ## no critic (Community::Wantarray)
492              
493 3         6 my %save;
494 3         19 my $apps = Test2::Tools::HTTP::Apps->new;
495              
496 3         12 foreach my $url (keys %h)
497             {
498 3   100     10 my $old = $apps->uri_to_app($url) || 1;
499 3         10 my $new = $h{$url};
500 3         9 $save{$url} = $old;
501 3 100       35 $apps->del_psgi($url) if ref $old;
502 3         12 $apps->add_psgi($url => $new);
503             }
504              
505 3         30 Test2::Tools::HTTP::Guard->new(%save);
506             }
507              
508             package Test2::Tools::HTTP::Guard;
509              
510             sub new
511             {
512 3     3   13 my($class, %save) = @_;
513 3         17 bless \%save, $class;
514             }
515              
516             sub restore
517             {
518 3     3   8 my($self) = @_;
519              
520 3         24 my $apps = Test2::Tools::HTTP::Apps->new;
521              
522 3         18 foreach my $url (keys %$self)
523             {
524 3         8 my $app = $self->{$url};
525 3         13 $apps->del_psgi($url);
526 3 100       23 $apps->add_psgi($url => $app)
527             if ref $app;
528             }
529             }
530              
531             sub DESTROY
532             {
533 3     3   48 my($self) = @_;
534 3         10 $self->restore;
535             }
536              
537             package Test2::Tools::HTTP::ResponseCompare;
538              
539 6     6   73 use parent 'Test2::Compare::Object';
  6         13  
  6         45  
540              
541 13     13   7831 sub name { '' }
542 39     39   7343 sub object_base { 'HTTP::Response' }
543              
544             sub init
545             {
546 40     40   1302 my($self) = @_;
547 40   50     291 $self->{HTTP_CHECK} ||= [];
548 40         173 $self->SUPER::init();
549             }
550              
551             sub add_http_check
552             {
553 55     55   1557 my($self, $cb, $id, $expect) = @_;
554              
555 55         83 push @{ $self->{HTTP_CHECK} }, [ $cb, $id, $expect ];
  55         310  
556             }
557              
558             sub deltas
559             {
560 38     38   214 my $self = shift;
561 38         172 my @deltas = $self->SUPER::deltas(@_);
562 38         5548 my %params = @_;
563              
564 38         231 my ($got, $convert, $seen) = @params{qw/got convert seen/};
565              
566 38         65 foreach my $pair (@{ $self->{HTTP_CHECK} })
  38         101  
567             {
568 55         2182 my($cb, $id, $check) = @$pair;
569              
570 55         92 my($val, $exists, $alt_check) = eval { $cb->($got) };
  55         129  
571 55         11717 my $error = $@;
572              
573 55 100       157 $check = $alt_check if defined $alt_check;
574              
575 55         178 $check = $convert->($check);
576              
577 55 50       6036 if($error)
578             {
579 0         0 push @deltas => $self->delta_class->new(
580             verified => undef,
581             id => $id,
582             got => undef,
583             check => $check,
584             exception => $error,
585             );
586             }
587             else
588             {
589 55 50       265 push @deltas => $check->run(
    100          
590             id => $id,
591             convert => $convert,
592             seen => $seen,
593             exists => $exists,
594             $exists ? ( got => $val eq '' ? '[empty string]' : $val ) : (),
595             );
596             }
597             }
598              
599 38         5874 @deltas;
600             }
601              
602             1;
603              
604             __END__