File Coverage

blib/lib/Browsermob/Proxy/CompareParams.pm
Criterion Covered Total %
statement 3 100 3.0
branch 0 26 0.0
condition 0 6 0.0
subroutine 1 13 7.6
pod 4 5 80.0
total 8 150 5.3


line stmt bran cond sub pod time code
1             package Browsermob::Proxy::CompareParams;
2             $Browsermob::Proxy::CompareParams::VERSION = '0.15';
3             # ABSTRACT: Look for a request with the specified matching request params
4 1     1   2461 use Carp qw/croak/;
  1         1  
  1         913  
5              
6             require Exporter;
7             our @ISA = qw/Exporter/;
8             our @EXPORT = qw/cmp_request_params/;
9             our @EXPORT_OK = qw/convert_har_params_to_hash
10             replace_placeholder_values
11             collect_query_param_keys/;
12              
13              
14              
15             sub cmp_request_params {
16 0     0 1   my ($got, $expected, $user_cmp) = @_;
17 0           my $got_hash = convert_har_params_to_hash($got);
18 0           my $compare = generate_comparison_sub($user_cmp);
19              
20             # Start by assuming that we can't find any of our expected keys
21 0           my @least_missing = keys %{ $expected };
  0            
22              
23 0           my @matched = grep {
24 0           my $actual_params = $_;
25              
26             # The @missing array will contain the expected keys that
27             # either do not exist in actual params, or they do exist but
28             # the values aren't the same.
29 0           my @missing = grep {
30 0           my $key = $_;
31             # Negative asserts ( "!missing", "!not_equal:to_this" )
32             # need to be handled differently
33 0 0         if ( _is_negative_assert($key) ) {
34 0           _assert_negative_kv($key, $expected->{$key}, $actual_params, $compare);
35             }
36             else {
37 0           _assert_positive_kv($key, $expected->{$key}, $actual_params, $compare);
38             }
39 0           } keys %{ $expected };
40              
41             # We need to keep track of the closest match we've found so
42             # far so we can tell the caller about it when we're done
43 0 0         if (scalar @missing < scalar @least_missing) {
44 0           @least_missing = @missing;
45             }
46              
47             # @missing will be empty for a successful request/assert
48             # match.
49 0           ! ( scalar @missing )
50 0           } @{ $got_hash };
51              
52 0 0         if (wantarray) {
53             # In list context, provide the closest match for context on
54             # the caller's side
55 0           my $missing = { map {
56 0           $_ => $expected->{$_}
57             } @least_missing };
58 0           return (scalar @matched, $missing);
59             }
60             else {
61 0           return scalar @matched;
62             }
63             }
64              
65             sub _is_negative_assert {
66 0     0     my ($key) = @_;
67              
68 0           return $key =~ /^!/;
69             }
70              
71             sub _assert_negative_kv {
72 0     0     my ($key, $expected, $actual_params, $compare) = @_;
73              
74             # Negative asserts come in two flavors: either the key must not
75             # exist at all, or the key must exist, but its value cannot match
76             # the expected.
77              
78 0 0         if ($expected eq '') {
79 0           return _assert_missing_key( $key, $actual_params );
80             }
81             else {
82 0           return _assert_different_value( $key, $expected, $actual_params, $compare );
83             }
84             }
85              
86             sub _assert_different_value {
87 0     0     my ($key, $expected, $actual_params, $compare) = @_;
88 0           my $actual_key = $key;
89 0           $actual_key =~ s/^!//;
90              
91 0 0         if ( exists $actual_params->{$actual_key} ) {
92             # At this point, we know the key exists, and we just want to
93             # make sure we _dont_ match our assertion. Which is to say,
94             # the exact opposite of a positive kv assertion.
95 0           return ! _assert_positive_kv( $actual_key, $expected, $actual_params, $compare);
96             }
97             else {
98             # An assert like "!missing: not this" requires that the key
99             # exists and is not equal to the value. If the key does not
100             # even exist, that is bad; we assert that it must exist.
101 0           return 'needs to exist';
102             }
103              
104 0           return $ret;
105             }
106              
107             sub _assert_missing_key {
108 0     0     my ($key, $actual_params) = @_;
109             # The key looks like "!query", but the actual key we are
110             # interested in is "query".
111 0           my $actual_key = $key;
112 0           $actual_key =~ s/^!//;
113              
114 0 0         if (exists $actual_params->{$actual_key}) {
115             # We're asserting that the key is not present. Since we've
116             # found it, that's bad; the grep up in cmp_request_params
117             # expects truthy values to indicate something bad.
118 0           return 'found';
119             }
120             else {
121             # The key isn't in the actual params, so we're good! False
122             # values indicate that everything is okay.
123 0           return '';
124             }
125             }
126              
127             sub _assert_positive_kv {
128 0     0     my ($key, $expected, $actual_params, $compare) = @_;
129              
130             # Start off assuming that the expected key is missing from the
131             # actual params.
132 0           my $ret = 'missing';
133              
134             # The expected key must exist in the actual params...
135 0 0         if ( exists $actual_params->{$key} ) {
136 0           my $got = $actual_params->{$key};
137             # and the expected key's value must match the actual param's
138             # key's value.
139 0 0         if ( $compare->( $got, $expected ) ) {
140 0           $ret = '';
141             }
142             }
143              
144             # Otherwise, we've initialized $ret as missing so we're good to go.
145 0           return $ret;
146             }
147              
148              
149             sub convert_har_params_to_hash {
150 0     0 1   my ($har_or_requests) = @_;
151              
152 0           my $requests;
153 0 0 0       if (ref($har_or_requests) eq 'HASH' && exists $har_or_requests->{log}->{entries}) {
154 0           $requests = $har_or_requests->{log}->{entries};
155             }
156             else {
157 0           $requests = $har_or_requests;
158             }
159              
160 0           my $hash = [
161             map {
162 0           my $params = $_->{request}->{queryString};
163 0           my $pairs = { map {
164 0           $_->{name} => $_->{value}
165             } @$params };
166              
167 0           $pairs
168 0           } @{ $requests }
169             ];
170              
171 0           return $hash;
172             }
173              
174             sub generate_comparison_sub {
175 0     0 0   my ($user_comparison) = @_;
176 0     0     my $string_equality = sub { $_[0] eq $_[1] };
  0            
177              
178 0 0         if (! defined $user_comparison) {
179 0           return $string_equality;
180             }
181              
182 0           my $ref = ref($user_comparison);
183 0 0         if ($ref ne 'CODE') {
184 0           croak 'We expected your custom comparison to be a CODEREF, not a ' . $ref . '!';
185             }
186              
187             return sub {
188 0     0     my ($got, $expected) = @_;
189              
190 0   0       return $string_equality->($got, $expected) || $user_comparison->($got, $expected);
191 0           };
192              
193             }
194              
195              
196             sub replace_placeholder_values {
197 0     0 1   my ($requests, $assert) = @_;
198              
199 0           my $mutated = { map {
200 0           my ($key, $value) = ($_, $assert->{$_});
201 0 0         if ($value !~ /^ *: */) {
202 0           $key => $value
203             }
204             else {
205 0           my $replacement_key = $value;
206 0           $replacement_key =~ s/^ *: *//;
207              
208 0           my $actual_keys = collect_query_param_keys($requests);
209 0           my $found_existing_key = scalar(
210 0           grep { $_ eq $replacement_key } @{ $actual_keys }
  0            
211             );
212 0 0         if ($found_existing_key) {
213 0           $key => $assert->{$replacement_key};
214             }
215             else {
216 0           $key => $value
217             }
218             }
219              
220 0           } keys %{ $assert } };
221              
222 0           return $mutated;
223             }
224              
225              
226             sub collect_query_param_keys {
227 0     0 1   my ($requests) = @_;
228              
229 0           my $kv_params = convert_har_params_to_hash($requests);
230              
231 0           my $keys = {};
232 0           foreach my $param_pairs (@{ $kv_params }) {
  0            
233 0           map { $keys->{$_}++ } keys %{ $param_pairs };
  0            
  0            
234             }
235              
236 0           return [ sort keys %{ $keys } ];
  0            
237             }
238              
239             1;
240              
241             __END__
242              
243             =pod
244              
245             =encoding UTF-8
246              
247             =head1 NAME
248              
249             Browsermob::Proxy::CompareParams - Look for a request with the specified matching request params
250              
251             =head1 VERSION
252              
253             version 0.15
254              
255             =head1 SYNOPSIS
256              
257             # create a har with traffic
258             my $ua = LWP::UserAgent->new;
259             my $proxy = Browsermob::Server->new->create_proxy;
260             $ua->proxy($proxy->ua_proxy);
261             $ua->get('http://www.perl.org/?query=string');
262             my $har = $proxy->har;
263              
264             # ask the har if any requests have the following query params
265             my $request_found = cmp_request_params($har, { query => 'string' });
266             if ($request_found) {
267             print 'A request was found with ?query=string in it';
268             }
269              
270             =head1 DESCRIPTION
271              
272             Our primary use of Browsermob::Proxy is for checking analytics
273             requests. They're transferred primarily in the form of request
274             parameters, so it behooves us to make it easy to check if our HAR has
275             any requests that match a set of our expected request params.
276              
277             By default, we only export the one function: L</cmp_request_params>.
278              
279             =head1 METHODS
280              
281             =head2 cmp_request_params ( $har, $expected_params )
282              
283             Pass in a $har object genereated by L</Browsermob::Proxy>, as well as
284             a hashref of key/value pairs of the request params that you want to
285             find. In scalar context, this method will return the number of
286             requests that can be found with all of the expected_params key/value
287             pairs. If no requests are found, it returns that number: 0. So, the
288             scalar context returns a boolean if we were able to find any matching
289             requests.
290              
291             # look for a request matching ?expected=params&go=here
292             my $bool = cmp_request_params($har, { expected => 'params', go => 'here' });
293             say 'We found it!' if $bool;
294              
295             In list context, the sub will return the boolean status as before, as
296             well as a hashref with the missing pieces from the closest request.
297              
298             my ($bool, $missing_params) = cmp_request_params($har, $expected);
299             if ( ! $bool ) {
300             say 'We are missing: ';
301             print Dumper $missing_params;
302             }
303              
304             =head2 convert_har_params_to_hash
305              
306             This isn't exported by default; we wouldn't expect that you'd need to
307             use it. But, if you're interested: the har format is a bit unwieldy to
308             work with. The requests come in an array of objects. Each object in
309             the array is a hash with a request key which points to an object with
310             a queryString key. The queryString object is an array of hashes with
311             name and value keys, the values of which are the actual query
312             params. Here's an example of one request:
313              
314             [0] {
315             ...
316             request {
317             ...
318             queryString [
319             [0] {
320             name "query",
321             value "string"
322             },
323             [1] {
324             name "query2",
325             value "string2"
326             },
327             ],
328             url "http://127.0.0.1/b/ss?query=string&query2=string2"
329             },
330             ...
331             }
332              
333             This function would transform that request into an array of hash
334             objects where the keys are the param names and the values are the
335             param values:
336              
337             \ [
338             [0] {
339             query "string"
340             query2 "string2"
341             }
342             ]
343              
344             =head1 FUNCTIONS
345              
346             =head2 replace_placeholder_values
347              
348             Takes two arguments: a HAR or the C<->{log}->{entries}> of a HAR, and
349             an assert hashref. If the assert has a value that starts with a colon
350             C<:>, and that value exists as a key in any of the HAR's actual query
351             parameter pairs, we'll replace the asserted value with the matching
352             assert's key.
353              
354             An example may help make this clear: say you assert the following
355             hashref
356              
357             $assert = {
358             query => 'param',
359             query2 => ':query'
360             };
361              
362             and your HAR records a request to a URL with the following params:
363             C</endpoint?query=param&query2=param>. We'll return you a new
364             C<$assert>:
365              
366             $assert = {
367             query => 'param',
368             query2 => 'param'
369             };
370              
371             =head2 collect_query_param_keys
372              
373             Given a HAR, or a the entries array of a HAR, we'll return a list of
374             all of the keys that were used in any of the query parameters. So if
375             your HAR contains a call to C</endpoint?example1&example2> and another
376             call to C</endpoint?example2&example3>, we'll return C<[ qw/ example1
377             example2 example3 ]>.
378              
379             =head1 SEE ALSO
380              
381             Please see those modules/websites for more information related to this module.
382              
383             =over 4
384              
385             =item *
386              
387             L<Browsermob::Proxy|Browsermob::Proxy>
388              
389             =back
390              
391             =head1 BUGS
392              
393             Please report any bugs or feature requests on the bugtracker website
394             https://github.com/gempesaw/Browsermob-Proxy/issues
395              
396             When submitting a bug or request, please include a test-file or a
397             patch to an existing test-file that illustrates the bug or desired
398             feature.
399              
400             =head1 AUTHOR
401              
402             Daniel Gempesaw <gempesaw@gmail.com>
403              
404             =head1 COPYRIGHT AND LICENSE
405              
406             This software is copyright (c) 2014 by Daniel Gempesaw.
407              
408             This is free software; you can redistribute it and/or modify it under
409             the same terms as the Perl 5 programming language system itself.
410              
411             =cut