File Coverage

blib/lib/Test/Async/HTTP.pm
Criterion Covered Total %
statement 82 91 90.1
branch 22 24 91.6
condition 1 3 33.3
subroutine 23 26 88.4
pod 6 7 85.7
total 134 151 88.7


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2014 -- leonerd@leonerd.org.uk
5              
6             package Test::Async::HTTP;
7              
8 6     6   83022 use strict;
  6         13  
  6         222  
9 6     6   26 use warnings;
  6         8  
  6         250  
10              
11             our $VERSION = '0.02';
12              
13 6     6   2882 use HTTP::Request;
  6         130340  
  6         3079  
14              
15             =head1 NAME
16              
17             C - unit test code that uses C
18              
19             =head1 DESCRIPTION
20              
21             This module implements a mock version of L suitable for unit
22             tests that virtualises the actual HTTP request/response cycle, allowing the
23             unit test script to inspect the requests made and provide responses to them.
24              
25             =cut
26              
27             # TODO: Move these into a class within the package
28              
29             sub new
30             {
31 5     5 0 56 my $class = shift;
32 5         20 bless { @_ }, $class
33             }
34              
35             =head1 METHODS
36              
37             =cut
38              
39             =head2 $f = $http->do_request( %args )
40              
41             Implements the actual L request API.
42              
43             The following arguments are handled specially:
44              
45             =over 4
46              
47             =item * timeout
48              
49             The value of a C argument is captured as an extra header on the
50             request object called C.
51              
52             =item * stall_timeout
53              
54             =item * expect_continue
55              
56             =item * SSL
57              
58             These arguments are entirely ignored.
59              
60             =back
61              
62             =cut
63              
64             # The main Net::Async::HTTP method
65             sub do_request
66             {
67 11     11 1 33917 my $self = shift;
68 11         42 my %args = @_;
69              
70 11 100       39 if( !exists $args{request} ) {
71 2         15 my $request = $args{request} = HTTP::Request->new(
72             delete $args{method}, delete $args{uri}
73             );
74 2 100       7312 $request->content( delete $args{content} ) if exists $args{content};
75             }
76              
77             my $pending = Test::Async::HTTP::Pending->new(
78             request => delete $args{request},
79             content => delete $args{request_body},
80 11 100       105 on_write => ( $args{on_body_write} ? do {
81 1         2 my $on_body_write = delete $args{on_body_write};
82 1         2 my $written = 0;
83 1     1   4 sub { $on_body_write->( $written += $_[0] ) }
84 1         12 } : undef ),
85             on_header => delete $args{on_header},
86             );
87              
88 11 100       130 if( my $timeout = delete $args{timeout} ) {
89             # Cheat - easier for the unit tests to find it here
90 1         3 $pending->request->header( "X-NaHTTP-Timeout" => $timeout );
91             }
92              
93 11         103 delete $args{expect_continue};
94 11         16 delete $args{SSL};
95              
96 11         15 delete $args{stall_timeout};
97              
98 11 50       49 die "TODO: more args: " . join( ", ", keys %args ) if keys %args;
99              
100 11         12 push @{ $self->{next} }, $pending;
  11         51  
101              
102 11         33 return $pending->response;
103             }
104              
105             =head2 $response = $http->GET( $uri, %args )->get
106              
107             =head2 $response = $http->HEAD( $uri, %args )->get
108              
109             =head2 $response = $http->PUT( $uri, $content, %args )->get
110              
111             =head2 $response = $http->POST( $uri, $content, %args )->get
112              
113             Convenient wrappers for using the C, C, C or C methods
114             with a C object and few if any other arguments, returning a C.
115              
116             Remember that C with non-form data (as indicated by a plain scalar
117             instead of an C reference of form data name/value pairs) needs a
118             C key in C<%args>.
119              
120             =cut
121              
122             sub GET
123             {
124 1     1 1 5 my $self = shift;
125 1         3 my ( $uri, @args ) = @_;
126 1         5 return $self->do_request( method => "GET", uri => $uri, @args );
127             }
128              
129             sub HEAD
130             {
131 0     0 1 0 my $self = shift;
132 0         0 my ( $uri, @args ) = @_;
133 0         0 return $self->do_request( method => "HEAD", uri => $uri, @args );
134             }
135              
136             sub PUT
137             {
138 1     1 1 971 my $self = shift;
139 1         3 my ( $uri, $content, @args ) = @_;
140 1         4 return $self->do_request( method => "PUT", uri => $uri, content => $content, @args );
141             }
142              
143             sub POST
144             {
145 0     0 1 0 my $self = shift;
146 0         0 my ( $uri, $content, @args ) = @_;
147 0         0 return $self->do_request( method => "POST", uri => $uri, content => $content, @args );
148             }
149              
150             =head2 $p = $http->next_pending
151              
152             Returns the next pending request wrapper object if one is outstanding (due to
153             an earlier call to C), or C.
154              
155             =cut
156              
157             sub next_pending
158             {
159 12     12 1 1796 my $self = shift;
160 12 100       16 my $pending = shift @{ $self->{next} } or return;
  12         46  
161              
162 11 100       31 if( defined $pending->content ) {
163 3         8 $pending->_pull_content( $pending->content );
164 3         15 undef $pending->content;
165             }
166              
167 11         70 return $pending;
168             }
169              
170             package Test::Async::HTTP::Pending;
171              
172             =head1 PENDING REQUEST OBJECTS
173              
174             Objects returned by C respond to the following methods:
175              
176             =cut
177              
178 6     6   4918 use Future;
  6         55333  
  6         2906  
179              
180             sub new
181             {
182 11     11   17 my $class = shift;
183 11         37 my %args = @_;
184 11         65 bless [
185             $args{request},
186             $args{content},
187             $args{on_write},
188             $args{on_header},
189             Future->new, # response
190             ], $class;
191             }
192              
193             =head2 $request = $p->request
194              
195             Returns the L object underlying this pending request.
196              
197             =cut
198              
199 15     15   514 sub request { shift->[0] }
200 17     17   108 sub content:lvalue { shift->[1] }
201 5     5   19 sub on_write { shift->[2] }
202 11     11   40 sub on_header { shift->[3] }
203 21     21   99 sub response { shift->[4] }
204              
205 3     3   20 sub on_chunk:lvalue { shift->[5] }
206              
207             sub _pull_content
208             {
209 6     6   6 my $self = shift;
210 6         7 my ( $content ) = @_;
211              
212 6 100 33     28 if( !ref $content ) {
    100          
    50          
213 4         9 $self->request->add_content( $content );
214 4 100       64 $self->on_write->( length $content ) if $self->on_write;
215             }
216             elsif( ref $content eq "CODE" ) {
217 1         4 while( defined( my $chunk = $content->() ) ) {
218 2         8 $self->_pull_content( $chunk );
219             }
220             }
221             elsif( blessed $content and $content->isa( "Future" ) ) {
222             $content->on_done( sub {
223 1     1   51 my ( $chunk ) = @_;
224 1         5 $self->_pull_content( $chunk );
225 1         7 });
226             }
227             else {
228 0         0 die "TODO: Not sure how to handle $content";
229             }
230             }
231              
232             =head2 $p->respond( $resp )
233              
234             Makes the request complete with the given L response. This
235             response is given to the Future that had been returned by the C
236             method.
237              
238             =cut
239              
240             sub respond
241             {
242 9     9   5188 my $self = shift;
243 9         16 my ( $response ) = @_;
244              
245 9 100       41 if( $self->on_header ) {
246             # Ugh - maybe there's a more efficient way
247 1         3 my $header = $response->clone;
248 1         214 $header->content("");
249              
250 1         18 my $on_chunk = $self->on_header->( $header );
251 1         9 $on_chunk->( $response->content );
252 1         11 $self->response->done( $on_chunk->() );
253             }
254             else {
255 8         26 $self->response->done( $response );
256             }
257             }
258              
259             =head2 $p->respond_header( $header )
260              
261             =head2 $p->respond_more( $data )
262              
263             =head2 $p->respond_done
264              
265             Alternative to the single C method, to allow an equivalent of chunked
266             encoding response. C responds with the header and initial
267             content, followed by multiple calls to C to provide more body
268             content, until a final C call finishes the request.
269              
270             =cut
271              
272             sub respond_header
273             {
274 1     1   64 my $self = shift;
275 1         4 my ( $header ) = @_;
276              
277 1         8 $self->on_chunk = $self->on_header->( $header );
278             }
279              
280             sub respond_more
281             {
282 1     1   736 my $self = shift;
283 1         2 my ( $chunk ) = @_;
284              
285 1         3 $self->on_chunk->( $chunk );
286             }
287              
288             sub respond_done
289             {
290 1     1   376 my $self = shift;
291              
292 1         3 $self->response->done( $self->on_chunk->() );
293             }
294              
295             sub fail
296             {
297 0     0     my $self = shift;
298              
299 0           $self->response->fail( @_ );
300             }
301              
302              
303             =head1 AUTHOR
304              
305             Paul Evans
306              
307             =cut
308              
309             0x55AA;