File Coverage

blib/lib/Web/Solid/Test/HTTPLists.pm
Criterion Covered Total %
statement 110 119 92.4
branch 20 28 71.4
condition n/a
subroutine 19 19 100.0
pod 2 2 100.0
total 151 168 89.8


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