File Coverage

blib/lib/LWP/UserAgent/Mockable.pm
Criterion Covered Total %
statement 93 98 94.9
branch 30 36 83.3
condition 5 5 100.0
subroutine 17 17 100.0
pod 6 6 100.0
total 151 162 93.2


line stmt bran cond sub pod time code
1             package LWP::UserAgent::Mockable;
2              
3 11     11   726160 use warnings;
  11         32  
  11         487  
4 11     11   62 use strict;
  11         23  
  11         409  
5              
6 11     11   11912 use Hook::LexWrap;
  11         41458  
  11         1353  
7 11     11   4390 use LWP::UserAgent;
  11         105124  
  11         447  
8 11     11   71 use Storable qw( dclone nstore retrieve );
  11         26  
  11         14660  
9              
10             our $VERSION = '1.10';
11              
12             my $instance = __PACKAGE__->__instance;
13             sub __instance {
14 11     11   26 my ( $class ) = @_;
15              
16 11 50       62 if ( not defined $instance ) {
17 11         71 $instance = bless {
18             action => undef,
19             file => undef,
20             current_request => undef,
21             actions => [],
22             callbacks => {},
23             wrappers => {},
24             }, $class;
25              
26             my $action = defined $ENV{ LWP_UA_MOCK }
27             ? lc $ENV{ LWP_UA_MOCK }
28 11 100       79 : 'passthrough';
29              
30 11         47 $instance->reset( $action, $ENV{ LWP_UA_MOCK_FILE } );
31             }
32              
33 9         351 return $instance;
34             }
35              
36             sub reset {
37 22     22 1 108 my ( $class, $action, $file ) = @_;
38              
39 22 50       117 if ( scalar @{ $instance->{ actions } } ) {
  22         154  
40 0         0 die "Can't reset state whilst pending actions. Need to call finish first";
41             }
42              
43 22 100       84 if ( not defined $action ) {
44 9         58 $action = "passthrough";
45             }
46              
47 22 50       161 if ( $action !~ /^(playback|record|passthrough)/ ) {
48 0         0 die "Action must be one of 'passthrough', 'playback' or 'record'";
49             }
50              
51 22 100 100     1589 if ( $action ne 'passthrough' and not defined $file ) {
52 1         27 die "No file defined. Should point to file you wish to record to or playback from";
53             }
54              
55 21         59 $instance->{ wrappers } = {};
56 21         78 $instance->{ action } = $action;
57 21         182 $instance->{ file } = $file;
58 21         42 $instance->{ callbacks } = {};
59              
60 21         79 $instance->__reset;
61             }
62              
63             sub __reset {
64 21     21   38 my ( $self ) = @_;
65              
66 21         139 my ( $action, $file, $callbacks, $wrappers )
67 21         41 = @{ $self }{ qw( action file callbacks wrappers ) };
68              
69 21 100       69 if ( $action eq 'playback' ) {
70 5         11 local $Storable::Eval = 1;
71              
72 5         29 $self->{ actions } = retrieve( $file );
73              
74             $wrappers->{ pre } = wrap LWP::UserAgent::simple_request,
75             pre => sub {
76 18     18   92036 my ( $wrapped, $request ) = @_;
77              
78 18         28 my $current = shift @{ $self->{ actions } };
  18         58  
79 18 50       65 if ( not defined $current ) {
80 0         0 die "No further HTTP requests exist. You possibly need to re-record the LWP session";
81             }
82              
83 18         39 my $response = $current->{ response };
84              
85 18 100       48 if ( $callbacks->{ playback_validation } ) {
86 3         6 my $mock_request = $current->{ request };
87              
88 3         29 $callbacks->{ playback_validation }( $request, $mock_request );
89             }
90              
91 18 100       263 if ( $callbacks->{ playback }) {
92 1         5 $response = $callbacks->{ playback }( $request, $response );
93              
94 1 50       40 if ( not UNIVERSAL::isa( $response, 'HTTP::Response' ) ) {
95 0         0 die "playback callback didn't return an HTTP::Response object";
96             }
97             }
98              
99 18         148 $_[ -1 ] = $response;
100 4         64554 };
101             } else {
102             $wrappers->{ pre } = wrap LWP::UserAgent::simple_request,
103             pre => sub {
104 27     27   320366 my ( $wrapped, $request ) = @_;
105              
106 27         114 $self->{ current_request } = $request;
107              
108 27 100       293 if ( $callbacks->{ pre_record } ) {
109 9         49 $_[ -1 ] = $callbacks->{ pre_record }( $request );
110              
111 9 100       612 if ( not UNIVERSAL::isa( $_[ -1 ], 'HTTP::Response' ) ) {
112 3         74 die "pre-record callback didn't return an HTTP::Response object";
113             }
114             }
115 16         177 };
116              
117             # It's intentional that wrap is called separately for this. We want the
118             # post action to always be called, even if the pre-action short-circuits
119             # the request. Otherwise, would need to duplicate the storing logic.
120             # This does mean that, when both pre- and post-record callbacks are being
121             # used, that the post-callback will take precedence.
122              
123             $wrappers->{ post } = wrap LWP::UserAgent::simple_request,
124             post => sub {
125 24     24   2447707 my $response = $_[ -1 ];
126 24 100       167 if ( $callbacks->{ record }) {
127             $response = $callbacks->{ record }(
128             $self->{ current_request },
129 9         60 $response
130             );
131              
132 9 100       387 if ( not UNIVERSAL::isa( $response, 'HTTP::Response' ) ) {
133 3         105 die "record callback didn't return an HTTP::Response object";
134             }
135             }
136              
137 21 100       131 if ( $action eq 'record' ) {
138 13         35 local $Storable::Eval = 1;
139 13         194 local $Storable::Deparse = 1;
140              
141             my $cloned = dclone {
142             request => $self->{ current_request },
143 13         1907 response => $response
144             };
145              
146 13         1976 push @{ $self->{ actions } }, $cloned;
  13         110  
147             }
148 16         777 };
149             }
150             }
151              
152             sub finished {
153 9     9 1 11531 my ( $class ) = @_;
154              
155 9         33 my $action = $instance->{ action };
156              
157 9 100 100     78 if ( $action eq 'record' ) {
    50          
158 3         8 local $Storable::Deparse = 1;
159 3         9 local $Storable::Eval = 1;
160              
161 3         27 nstore $instance->{ actions }, $instance->{ file };
162 4         23 } elsif ( $action eq 'playback' and scalar @{ $instance->{ actions } } ) {
163 0         0 warn "Not all HTTP requests have been played back. You possibly need to re-record the LWP session";
164             }
165              
166 9         4379 $instance->{ actions } = [];
167 9         191 $instance->{ action } = 'passthrough';
168 9         24 $instance->{ file } = undef;
169              
170 9         48 $instance->reset;
171             }
172              
173             sub set_playback_callback {
174 2     2 1 22890 my ( $class, $cb ) = @_;
175              
176 2         11 $instance->__set_cb( playback => $cb );
177             }
178              
179             sub set_record_callback {
180 15     15 1 19887 my ( $class, $cb ) = @_;
181              
182 15         66 $instance->__set_cb( record => $cb );
183             }
184              
185             sub set_record_pre_callback {
186 12     12 1 26393 my ( $class, $cb ) = @_;
187              
188 12         46 $instance->__set_cb( pre_record => $cb );
189             }
190              
191             sub set_playback_validation_callback {
192 1     1 1 1115 my ( $class, $cb ) = @_;
193              
194 1         4 $instance->__set_cb( playback_validation => $cb );
195             }
196              
197             sub __set_cb {
198 30     30   62 my ( $self, $type, $cb ) = @_;
199              
200 30         388 $self->{ callbacks }{ $type } = $cb;
201             }
202              
203             1;
204              
205             __END__
206              
207             =head1 NAME
208              
209             LWP::UserAgent::Mockable - Permits recording, and later playing back of LWP requests.
210              
211             =head1 VERSION
212              
213             Version 1.10
214              
215             =head1 SYNOPSIS
216              
217             # setup env vars to control behaviour, allowing them to be
218             # overridden from command line. In current case, do before
219             # loading module, so will be actioned on.
220            
221             BEGIN {
222             $ENV{ LWP_UA_MOCK } ||= 'playback';
223             $ENV{ LWP_UA_MOCK_FILE } ||= 'lwp-mock.out';
224             }
225              
226             use LWP;
227             use LWP::UserAgent::Mockable;
228              
229             # setup a callback when recording, to allow modifying the response
230              
231             LWP::UserAgent::Mockable->set_record_callback( sub {
232             my ( $request, $response ) = @_;
233              
234             print "GOT REQUEST TO: " . $request->uri;
235             $response->content( lc( $response->content ) );
236              
237             return $response;
238             } );
239              
240             # perform LWP request, as normal
241              
242             my $ua = LWP::UserAgent->new;
243             my $res = $ua->get( "http://gmail.com" );
244             print $res->content;
245              
246             # when the LWP work is done, inform LWP::UserAgent::Mockable
247             # that we're finished. Will trigger any behaviour specific to
248             # the action being done, such as saving the recorded session.
249            
250             LWP::UserAgent::Mockable->finished;
251              
252             =head1 DESCRIPTION
253              
254             This module adds session record and playback options for LWP requests, whilst trying to introduce as little clutter as necessary. When in record mode, all LWP requests and responses will be captured in-memory, until the finished method is called, at which point they will then be written out to a file. In playback mode, LWP responses are short-circuited, to instead return the responses that were previously dumped out. If neither of the above actions are requested, this module does nothing, so LWP requests are handled as normal.
255              
256             Most of the control of this module is done via environment variables, both to control the action being done (LWP_UA_MOCK env var, allowed values being 'record', 'playback', 'passthrough' (the default) ), and to control the file that is used for storing or replaying the responses (LWP_UA_MOCK_FILE env var, not used for 'passthrough' mode).
257              
258             The only mandatory change to incorporate this module is to call the 'finished' method, to indicate that LWP processing is completed. Other than that, LWP handling can be done as-normal.
259              
260             As the initial impetus for this module was to allow mocking of external HTTP calls within unittests, a couple of optional callback (one for each action of the valid action types), to allow for custom handling of responses, or to modify the response that is returned back to the client (this is useful for simulating the requested system being down, or when playing back, to modify the mocked response to reflect expected dynamic content).
261              
262             =head2 Methods
263              
264             As there is only a singleton instance of LWP::UserAgent::Mockable, all methods are class methods.
265              
266             =over 4
267              
268             =item finished() - required
269              
270             Informs LWP::UserAgent::Mockable that no further requests are expected, and allow it to do any post-processing that is required.
271              
272             When in 'record' mode, this will cause the playback file (controlled by LWP_UA_MOCK_FILE env var) to be created. When in 'playback' mode, this will issue a warning if there is still additional mocked responses that haven't been returned.
273              
274             =item set_record_callback( <sub {}> ) - optional
275              
276             =item set_playback_callback( <sub {}> ) - optional
277              
278             These optional methods allow custom callbacks to be inserted, when performing the relevant actions. The callback will be invoked for each LWP request, AFTER the request has been actioned (see set_record_pre_callback for a method of short-circuiting the LWP fetch). They will be passed in 2 parameters, an L<HTTP::Request> and an L<HTTP::Response> object. For the record callback (which is used for both 'record' and 'passthrough' mode) the request will be the L<HTTP::Request> object used to perform the request, and the response the L<HTTP::Response> result from that. In playback mode, the request will be the L<HTTP::Request> object used to perform the request, and the response the mocked response object.
279              
280             When the callbacks are being used, they're expected to return an L<HTTP::Response> object, which will be treated as the actual reply from the call being made. Failure to do do will result in a fatal error being raised.
281              
282             To clear a callback, call the relevant method, passing in no argument.
283              
284             =item set_record_pre_callback( <sub {}> ) - optional
285              
286             This callback is similar to set_record_callback, except that it will short-circuit the actual fetching of the remote URL. Only a single parameter is passed through to this callback, that being the L<HTTP::Request> object. It's expected to construct an return an L<HTTP::Response> object (or subclass thereof). Should anything other than an L<HTTP::Response> subclass be returned, a fatal error will be raised.
287              
288             This callback will be invoked for both 'record' and 'passthrough' modes. Note that there is no analagous callback for 'playback' mode.
289              
290             To clear the callback, pass in no argument.
291              
292             =item set_playback_validation_callback( <sub {}> ) - optional
293              
294             This callback allows validation of the received request. It receives two parameters, both L<HTTP::Request>s, the first being the actual request made, the second being the mocked request that was received when recording a session. It's up to the callback to do any validation that it wants, and to perform any action that is warranted.
295              
296             As with other callbacks, to clear, pass in no argument to the method.
297              
298             =item reset( <action>, <file> ) - optional
299              
300             Reset the state of mocker, allowing the action and file operation on to change. Will also reset all callbacks. Note that this will raise an error, if called whilst there are outstanding requests, and the B<finished> method hasn't been called.
301              
302             =back
303              
304             =head1 CAVEATS
305              
306             The playback file generated by this is not encrypted in any manner. As it's only using L<Storable> to dump the file, it's easy to get at the data contained within, even if the requests are going to HTTPS sites. Treat the playback file as if it were the original data, security-wise.
307              
308             =head1 SEE ALSO
309              
310             L<LWP::UserAgent> - The class being mocked.
311              
312             L<HTTP::Request>
313              
314             L<HTTP::Response>
315              
316             =head1 AUTHOR
317              
318             Mark Morgan, C<< <makk384@gmail.com> >>
319              
320             =head1 BUGS
321              
322             Please report any bugs or feature requests to C<bug-lwp-useragent-mockable at rt.cpan.org>, or through
323             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=LWP-UserAgent-Mockable>. I will be notified, and then you'll
324             automatically be notified of progress on your bug as I make changes.
325              
326             =head1 SUPPORT
327              
328             You can find documentation for this module with the perldoc command.
329              
330             perldoc LWP::UserAgent::Mockable
331              
332             You can also look for information at:
333              
334             =over 4
335              
336             =item * RT: CPAN's request tracker
337              
338             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=LWP-UserAgent-Mockable>
339              
340             =item * AnnoCPAN: Annotated CPAN documentation
341              
342             L<http://annocpan.org/dist/LWP-UserAgent-Mockable>
343              
344             =item * CPAN Ratings
345              
346             L<http://cpanratings.perl.org/d/LWP-UserAgent-Mockable>
347              
348             =item * Search CPAN
349              
350             L<http://search.cpan.org/dist/LWP-UserAgent-Mockable/>
351              
352             =back
353              
354             =head1 COPYRIGHT & LICENSE
355              
356             Copyright 2009 Mark Morgan, all rights reserved.
357              
358             This program is free software; you can redistribute it and/or modify it
359             under the same terms as Perl itself.
360              
361             =cut