File Coverage

blib/lib/Test/Async/HTTP.pm
Criterion Covered Total %
statement 70 73 95.8
branch 18 20 90.0
condition 1 3 33.3
subroutine 20 21 95.2
pod 2 3 66.6
total 111 120 92.5


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 5     5   83385 use strict;
  5         9  
  5         198  
9 5     5   23 use warnings;
  5         6  
  5         1711  
10              
11             our $VERSION = '0.01';
12              
13             =head1 NAME
14              
15             C - unit test code that uses C
16              
17             =head1 DESCRIPTION
18              
19             This module implements a mock version of L suitable for unit
20             tests that virtualises the actual HTTP request/response cycle, allowing the
21             unit test script to inspect the requests made and provide responses to them.
22              
23             =cut
24              
25             # TODO: Move these into a class within the package
26              
27             sub new
28             {
29 4     4 0 41 my $class = shift;
30 4         16 bless { @_ }, $class
31             }
32              
33             =head1 METHODS
34              
35             =cut
36              
37             =head2 $f = $http->do_request( %args )
38              
39             Implements the actual L request API.
40              
41             The following arguments are handled specially:
42              
43             =over 4
44              
45             =item * timeout
46              
47             The value of a C argument is captured as an extra header on the
48             request object called C.
49              
50             =item * stall_timeout
51              
52             =item * expect_continue
53              
54             =item * SSL
55              
56             These arguments are entirely ignored.
57              
58             =back
59              
60             =cut
61              
62             # The main Net::Async::HTTP method
63             sub do_request
64             {
65 9     9 1 31730 my $self = shift;
66 9         34 my %args = @_;
67              
68             my $pending = Test::Async::HTTP::Pending->new(
69             request => delete $args{request},
70             content => delete $args{request_body},
71 9 100       74 on_write => ( $args{on_body_write} ? do {
72 1         2 my $on_body_write = delete $args{on_body_write};
73 1         2 my $written = 0;
74 1     1   3 sub { $on_body_write->( $written += $_[0] ) }
75 1         12 } : undef ),
76             on_header => delete $args{on_header},
77             );
78              
79 9 100       104 if( my $timeout = delete $args{timeout} ) {
80             # Cheat - easier for the unit tests to find it here
81 1         2 $pending->request->header( "X-NaHTTP-Timeout" => $timeout );
82             }
83              
84 9         97 delete $args{expect_continue};
85 9         12 delete $args{SSL};
86              
87 9         11 delete $args{stall_timeout};
88              
89 9 50       29 die "TODO: more args: " . join( ", ", keys %args ) if keys %args;
90              
91 9         111 push @{ $self->{next} }, $pending;
  9         31  
92              
93 9         29 return $pending->response;
94             }
95              
96             =head2 $p = $http->next_pending
97              
98             Returns the next pending request wrapper object if one is outstanding (due to
99             an earlier call to C), or C.
100              
101             =cut
102              
103             sub next_pending
104             {
105 10     10 1 2542 my $self = shift;
106 10 100       14 my $pending = shift @{ $self->{next} } or return;
  10         48  
107              
108 9 100       23 if( defined $pending->content ) {
109 3         7 $pending->_pull_content( $pending->content );
110 3         22 undef $pending->content;
111             }
112              
113 9         34 return $pending;
114             }
115              
116             package Test::Async::HTTP::Pending;
117              
118             =head1 PENDING REQUEST OBJECTS
119              
120             Objects returned by C respond to the following methods:
121              
122             =cut
123              
124 5     5   3422 use Future;
  5         66867  
  5         3416  
125              
126             sub new
127             {
128 9     9   14 my $class = shift;
129 9         28 my %args = @_;
130 9         53 bless [
131             $args{request},
132             $args{content},
133             $args{on_write},
134             $args{on_header},
135             Future->new, # response
136             ], $class;
137             }
138              
139             =head2 $request = $p->request
140              
141             Returns the L object underlying this pending request.
142              
143             =cut
144              
145 12     12   100 sub request { shift->[0] }
146 15     15   65 sub content:lvalue { shift->[1] }
147 5     5   15 sub on_write { shift->[2] }
148 9     9   27 sub on_header { shift->[3] }
149 17     17   73 sub response { shift->[4] }
150              
151 3     3   17 sub on_chunk:lvalue { shift->[5] }
152              
153             sub _pull_content
154             {
155 6     6   6 my $self = shift;
156 6         6 my ( $content ) = @_;
157              
158 6 100 33     26 if( !ref $content ) {
    100          
    50          
159 4         7 $self->request->add_content( $content );
160 4 100       53 $self->on_write->( length $content ) if $self->on_write;
161             }
162             elsif( ref $content eq "CODE" ) {
163 1         4 while( defined( my $chunk = $content->() ) ) {
164 2         28 $self->_pull_content( $chunk );
165             }
166             }
167             elsif( blessed $content and $content->isa( "Future" ) ) {
168             $content->on_done( sub {
169 1     1   17 my ( $chunk ) = @_;
170 1         3 $self->_pull_content( $chunk );
171 1         7 });
172             }
173             else {
174 0         0 die "TODO: Not sure how to handle $content";
175             }
176             }
177              
178             =head2 $p->respond( $resp )
179              
180             Makes the request complete with the given L response. This
181             response is given to the Future that had been returned by the C
182             method.
183              
184             =cut
185              
186             sub respond
187             {
188 7     7   3268 my $self = shift;
189 7         11 my ( $response ) = @_;
190              
191 7 100       20 if( $self->on_header ) {
192             # Ugh - maybe there's a more efficient way
193 1         5 my $header = $response->clone;
194 1         225 $header->content("");
195              
196 1         18 my $on_chunk = $self->on_header->( $header );
197 1         8 $on_chunk->( $response->content );
198 1         12 $self->response->done( $on_chunk->() );
199             }
200             else {
201 6         14 $self->response->done( $response );
202             }
203             }
204              
205             =head2 $p->respond_header( $header )
206              
207             =head2 $p->respond_more( $data )
208              
209             =head2 $p->respond_done
210              
211             Alternative to the single C method, to allow an equivalent of chunked
212             encoding response. C responds with the header and initial
213             content, followed by multiple calls to C to provide more body
214             content, until a final C call finishes the request.
215              
216             =cut
217              
218             sub respond_header
219             {
220 1     1   63 my $self = shift;
221 1         2 my ( $header ) = @_;
222              
223 1         3 $self->on_chunk = $self->on_header->( $header );
224             }
225              
226             sub respond_more
227             {
228 1     1   784 my $self = shift;
229 1         2 my ( $chunk ) = @_;
230              
231 1         3 $self->on_chunk->( $chunk );
232             }
233              
234             sub respond_done
235             {
236 1     1   398 my $self = shift;
237              
238 1         4 $self->response->done( $self->on_chunk->() );
239             }
240              
241             sub fail
242             {
243 0     0     my $self = shift;
244              
245 0           $self->response->fail( @_ );
246             }
247              
248              
249             =head1 AUTHOR
250              
251             Paul Evans
252              
253             =cut
254              
255             0x55AA;