File Coverage

blib/lib/Web/Solid/Test/HTTPLists.pm
Criterion Covered Total %
statement 114 125 91.2
branch 21 30 70.0
condition n/a
subroutine 19 19 100.0
pod 2 2 100.0
total 156 176 88.6


line stmt bran cond sub pod time code
1             package Web::Solid::Test::HTTPLists;
2              
3 5     5   2237769 use 5.010001;
  5         47  
4 5     5   30 use strict;
  5         23  
  5         107  
5 5     5   24 use warnings;
  5         9  
  5         161  
6 5     5   782 use parent 'Test::FITesque::Fixture';
  5         494  
  5         28  
7 5     5   8615 use Test::More;
  5         97  
  5         92  
8 5     5   3111 use LWP::UserAgent;
  5         79848  
  5         134  
9 5     5   1978 use Test::Deep;
  5         28650  
  5         30  
10 5     5   3099 use Test::RDF;
  5         4913576  
  5         47  
11 5     5   853 use Data::Dumper;
  5         9  
  5         2734  
12              
13             our $AUTHORITY = 'cpan:KJETILK';
14             our $VERSION = '0.021';
15              
16             my $bearer_predicate = 'http://example.org/httplist/param#bearer'; # TODO: Define proper URI
17              
18             sub http_req_res_list_regex_reuser : Test : Plan(1) {
19 2     2 1 5659 my ($self, $args) = @_;
20 2         6 my @pairs = @{$args->{'-special'}->{'http-pairs'}}; # Unpack for readability
  2         8  
21 2         4 my @matches;
22             subtest $args->{'-special'}->{description} => sub {
23 2     2   2226 plan tests => scalar @pairs;
24 2         1342 my $ua = LWP::UserAgent->new(ssl_opts => { SSL_fingerprint => $ENV{SOLID_SSL_FINGERPRINT} } ); # TODO: Fix if it breaks when using CA certs
25             subtest "First request" => sub {
26 2         1856 my $request_no = 0;
27 2         6 my $request = $pairs[$request_no]->{request};
28 2 50       14 if ($args->{$bearer_predicate}) {
29 0         0 $request->header( 'Authorization' => _create_authorization_field($args->{$bearer_predicate}, $request->uri));
30             }
31              
32 2         21 my $response = $ua->request( $request );
33 2         85844 my $expected_response = $pairs[$request_no]->{response};
34 2         5 my $regex_fields = $pairs[$request_no]->{'regex-fields'};
35 2         12 my @expected_header_fields = $expected_response->header_field_names;
36 2         86 foreach my $expected_header_field (@expected_header_fields) { # TODO: Date-fields may fail if expectation is dynamic
37 3 100       12 if ($regex_fields->{$expected_header_field}) { # Then, we have a regular expression from the RDF to match
38 2         7 my $regex = $expected_response->header($expected_header_field);
39 2         76 like($response->header($expected_header_field), qr/$regex/, "\'$expected_header_field\'-header matches given regular expression");
40 2         926 my @res_matches = $response->header($expected_header_field) =~ m/$regex/;
41 2         111 push(@matches, \@res_matches);
42 2         8 $expected_response->remove_header($expected_header_field); # So that we can test the rest with reusable components
43             }
44             }
45              
46 2         47 _subtest_compare_req_res($request, $response, $expected_response);
47              
48 2         502 };
49              
50             # ASSUME: The first request sets the relative URI that can be used by subsequent requests
51             # ASSUME: The first match of the first request is the relative URI to be used for the this request
52 2         10037 my $relative = $matches[0]->[0];
53 2         9 for (my $request_no = 1; $request_no < @pairs; $request_no++) {
54 3         2347 my $pair = $pairs[$request_no];
55             subtest "Request-response #" . ($request_no+1) => sub {
56 3         2732 my $request = $pair->{request};
57 3 50       10 unless (defined($request->uri)) {
58             # ASSUME: RequestURI was not given, it has to be derived from the previous request through a match
59             # TODO: What if the match was absolute, not relative?
60 3 50       36 if (defined($relative)) {
61             # ASSUME: The base URI is the RequestURI for the first request
62 3         9 my $uri = URI->new_abs($relative, $pairs[0]->{request}->uri);
63 3         865 $request->uri($uri);
64             } else {
65 0         0 fail("No relative URI was found in the first test");
66 0         0 return;
67             }
68             }
69 3 50       87 if ($args->{$bearer_predicate}) {
70 0         0 $request->header( 'Authorization' => _create_authorization_field($args->{$bearer_predicate}, $request->uri));
71             }
72 3         10 my $response = $ua->request($request);
73 3         17786 my $expected_response = $pairs[$request_no]->{response};
74 3         10 _subtest_compare_req_res($request, $response, $expected_response);
75 3         24 };
76             }
77 2         30 };
78 5     5   36 }
  5         22  
  5         38  
79              
80             sub http_req_res_list : Test : Plan(1) {
81 2     2 1 3296 my ($self, $args) = @_;
82 2         4 my @pairs = @{$args->{'-special'}->{'http-pairs'}}; # Unpack for readability
  2         8  
83 2         18 my $ua = LWP::UserAgent->new(ssl_opts => { SSL_fingerprint => $ENV{SOLID_SSL_FINGERPRINT} } ); # TODO: Fix if it breaks when using CA certs
84             subtest $args->{'-special'}->{description} => sub {
85 2     2   2220 plan tests => scalar @pairs;
86 2         1368 my $counter = 1;
87 2         5 foreach my $pair (@pairs) {
88 3         8 my $request = $pair->{request};
89 3         11 _check_origin($request);
90 3 50       19 if ($args->{$bearer_predicate}) {
91 0         0 $request->header( 'Authorization' => _create_authorization_field($args->{$bearer_predicate}, $request->uri));
92             }
93 3         25 my $response = $ua->request( $request );
94             subtest "Request-response #" . ($counter) =>
95 3         98120 \&_subtest_compare_req_res, $request, $response, $pair->{response}; #Callback syntax isn't pretty, admittedly
96 3         14014 $counter++;
97             }
98 2         500 };
99 5     5   2244 }
  5         10  
  5         25  
100              
101              
102             sub _subtest_compare_req_res {
103 8     8   3563 my ($request, $response, $expected_response) = @_;
104 8         55 isa_ok($response, 'HTTP::Response');
105 8 100       3972 if ($expected_response->code) {
106 7         90 my $code = $expected_response->code;
107 7 50       62 like($response->code, qr/$code/, "Response code matches " . $expected_response->code)
108             || note "Returned content:\n" . $response->as_string;
109             }
110 8         2693 my @expected_header_fields = $expected_response->header_field_names;
111 8 100       233 if (scalar @expected_header_fields) {
112             subtest 'Testing all headers' => sub {
113 3     3   2921 plan tests => scalar @expected_header_fields;
114 3         2160 foreach my $expected_header_field (@expected_header_fields) { # TODO: Date-fields may fail if expectation is dynamic
115 3 50       13 if (defined($response->header($expected_header_field))) {
116             # The following line is a hack to parse field values
117             # with multiple values. Comma-separated lists are a
118             # common occurence, but as of RFC7230, they are not
119             # defined in the HTTP standard itself, it is left to
120             # each individual spec to define the syntax if the
121             # field values, so it is an open world. It would
122             # therefore be inappropriate to implement just
123             # splitting by comma (and whitespace) in a general
124             # purpose framework, even though it will work in most
125             # cases. Since it works for us now it makes sense to
126             # implement it as such for now. A more rigorous
127             # solution to the problem is in
128             # https://metacpan.org/pod/HTTP::Headers::ActionPack,
129             # which is an extensible framework for working with
130             # headers, and so, it can be used to implement syntax
131             # for headers that are seen.
132 3         131 my $tmp_h = HTTP::Headers->new($expected_header_field => [split(/,\s*/,$response->header($expected_header_field))]);
133             # TODO: Resolve relative URIs in the response
134 3         233 cmp_deeply([$tmp_h->header($expected_header_field)],
135             supersetof($expected_response->header($expected_header_field)),
136             "$expected_header_field is a superset as expected");
137             } else {
138 0         0 fail("Presence of $expected_header_field in response") # Easiest way to maintain correct number of tests and also not get a warning for a calling split on undef is to fail the test like this
139             }
140             }
141 3         24 };
142             } else {
143 5         42 note "No expected headers set";
144             }
145             }
146              
147             sub _create_authorization_field {
148 3     3   260743 my ($object, $request_url) = @_;
149 3 100       44 if ($object->isa('URI')) {
150 2         17 my $ua = LWP::UserAgent->new; # This UA is used towards the test IDP and should fail if the certs are invalid
151             # Construct the URI to retrieve bearer token from
152 2         2683 my $bearer_url = $object;
153 2 100       9 if (defined($request_url)) {
154             # If the request URL (i.e. to the resource under test is given, then set audience
155 1         4 my $aud_url = URI->new;
156 1         47 $aud_url->scheme($request_url->scheme);
157 1         177 $aud_url->authority($request_url->authority);
158 1         43 $bearer_url->query("aud=$aud_url");
159             }
160 2         53 my $response = $ua->get($bearer_url);
161 2 50       204382 BAIL_OUT 'Could not retrieve bearer token, got error ' . $response->as_string unless $response->is_success;
162 2         38 $object = $response->content;
163             }
164 3         439 return "Bearer $object";
165             }
166            
167             sub _check_origin {
168 3     3   5 my $request = shift;
169 3 100       22 if ($request->header('Origin')) {
170 1         45 my $origin = URI->new($request->header('Origin'));
171 1 50       983 if ($origin->path) {
172 0         0 note('Origin had path "' . $origin->path . '". Probably unproblematic. Using only scheme and authority');
173 0         0 my $new_origin = URI->new;
174 0         0 $new_origin->scheme($origin->scheme);
175 0         0 $new_origin->authority($origin->authority);
176 0         0 $request->header('Origin' => $new_origin->as_string);
177             }
178             }
179 3         253 return $request;
180             }
181              
182             1;
183              
184             __END__
185              
186             =pod
187              
188             =encoding utf-8
189              
190             =head1 NAME
191              
192             Web::Solid::Test::HTTPLists - Solid Tests using HTTP objects
193              
194             =head1 SYNOPSIS
195              
196             use Test::FITesque::RDF;
197             my $suite = Test::FITesque::RDF->new(source => $file, base_uri => $ENV{SOLID_REMOTE_BASE})->suite;
198             $suite->run_tests;
199             done_testing;
200              
201             A script C<tests/httplists.t> can be used to launch some of these tests.
202              
203             =head1 DESCRIPTION
204              
205             =head2 Introduction
206              
207             The basic idea with these tests is to simplify reuse and formulation
208             of fixture tables using the Resource Description Framework (RDF), in
209             this case using HTTP vocabularies to formulate lists of requests and
210             responses. It is in an early stage, but there are running tests in
211             this module. See L<Web::Solid::Test> for more on the
212             philosophy.
213              
214             This system is built on L<Test::FITesque::RDF>, which adds RDF fixture
215             tables to L<Test::FITesque>.
216              
217             =head1 IMPLEMENTED TESTS
218              
219             Apart from some author tests in this module, examples of actual tests
220             can be found in the L<Solid Test Suite|https://github.com/solid/test-suite>.
221              
222              
223             =head2 Test scripts
224              
225             In general, tests are formulated in RDF fixture tables, which
226             parameterizes the test cases. This parameterization is then given to
227             the test scripts. It is intended therefore that only a small number of
228             fairly general test scripts will be needed to provide an extensive
229             test suite.
230              
231             These are the test scripts implemented in this module:
232              
233              
234             =head2 C<< http_req_res_list >>
235              
236             Runs a list of HTTP request response pairs, checking response against the response.
237              
238             =head3 Parameters
239              
240             =over
241              
242             =item * C<test:steps>
243              
244             A list of request-response pairs, declared using:
245              
246             =over
247              
248             =item * C<test:request>
249              
250             An RDF list of requests that will be executed towards the server in C<SOLID_REMOTE_BASE>.
251              
252             =item * C<test:response_assertion>
253              
254             An RDF list of responses that will be used as corresponding expected responses in the tests.
255              
256             =back
257              
258             =item * C<http://example.org/httplist/param#bearer>
259              
260             A bearer token that if present will be used to authenticate the
261             requests given by the above list. The object of this predicate can
262             either be a literal bearer token, or a URL, in which case, it will be
263             dereferenced and the content will be used as the bearer token.
264              
265             =back
266              
267             =head3 Environment
268              
269             None
270              
271             =head3 Implements
272              
273             =over
274              
275             =item 1. That responses are L<HTTP::Response> objects.
276              
277             =item 2. That the response code matches the expected one if given. A regular expression may be used.
278              
279             =item 3. That all headers given in the asserted response matches a
280             header in the actual response.
281              
282             =back
283              
284              
285             =head2 C<< http_req_res_list_regex_reuser >>
286              
287             Runs a list of two HTTP request response pairs, using a regular
288             expression from the first request to set the request URL of the
289             second.
290              
291             =head3 Parameters
292              
293             Uses C<test:steps> like above.
294              
295             Additionally, the first request may have a regular expression that can
296             be used to parse data for the next request(s). To examine the Link
297             header, a response message can be formulated like (note, it practice
298             it would be more complex):
299              
300             :check_acl_location_res a http:ResponseMessage ;
301             httph:link '<(.*?)>;\\s+rel="acl"'^^dqm:regex ;
302             http:status 200 .
303              
304             The resulting match is placed in an array that will be used to set the
305             Request URI of the next request(s).
306              
307              
308             =head3 Environment
309              
310             None
311              
312             =head3 Implements
313              
314             =over
315              
316             =item 1. That the regular expression in the first request matches.
317              
318             =item 2. That responses are L<HTTP::Response> objects.
319              
320             =item 3. That the response code matches the expected one if given.
321              
322             =item 4. That headers that are not matched as regular expression but
323             given in the asserted response matches a header in the actual
324             response.
325              
326             =back
327              
328              
329             =head3 Assumptions
330              
331             See the source for details.
332              
333              
334              
335              
336             =head1 NOTE
337              
338             The parameters above are in the RDF formulated as actual full URIs,
339             but where the local part is used here and resolved by the
340             L<Test::FITesque::RDF> framework, see its documentation for details.
341              
342             To run tests against a server that uses HTTPS but uses a self-signed
343             certificate, the certificate's fingerprint needs to be supplied to the
344             test script to use in the validation. To set this, use the environment
345             variable C<SOLID_SSL_FINGERPRINT>. The fingerprint can be obtained for
346             example by visiting the system under test in a browser and examine the
347             certificate details from there.
348              
349             There are two SSL implementations in wide use, L<Net::SSL> and
350             L<IO::Socket::SSL>. The latter has largely supplantet the former, and
351             the the former has been known to cause the test suite to "hang" for
352             two minutes. Nevertheless, certain setups may still have it as the
353             default. To ensure that L<IO::Socket::SSL> is used, the environment
354             variable C<PERL_NET_HTTPS_SSL_SOCKET_CLASS> can be set to
355             C<IO::Socket::SSL>
356              
357              
358              
359             =head1 TODO
360              
361             The namespaces used in the current fixture tables are examples, and
362             will be changed before an 1.0 release of the system.
363              
364              
365             =head1 BUGS
366              
367             Please report any bugs to
368             L<https://github.com/kjetilk/p5-web-solid-test-basic/issues>.
369              
370             =head1 AUTHOR
371              
372             Kjetil Kjernsmo E<lt>kjetilk@cpan.orgE<gt>.
373              
374             =head1 COPYRIGHT AND LICENCE
375              
376             This software is Copyright (c) 2019, 2020 by Inrupt Inc.
377              
378             This is free software, licensed under:
379              
380             The MIT (X11) License
381              
382              
383             =head1 DISCLAIMER OF WARRANTIES
384              
385             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
386             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
387             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
388