File Coverage

blib/lib/Test/FITesque/RDF.pm
Criterion Covered Total %
statement 152 152 100.0
branch 36 36 100.0
condition 9 9 100.0
subroutine 23 23 100.0
pod 1 1 100.0
total 221 221 100.0


line stmt bran cond sub pod time code
1 13     13   996149 use 5.014;
  13         132  
2 13     13   61 use strict;
  13         22  
  13         265  
3 13     13   51 use warnings;
  13         19  
  13         689  
4              
5             package Test::FITesque::RDF;
6              
7             our $AUTHORITY = 'cpan:KJETILK';
8             our $VERSION = '0.016';
9              
10 13     13   6002 use Moo;
  13         122230  
  13         60  
11 13     13   20911 use Attean::RDF;
  13         21004365  
  13         84  
12 13     13   10700 use Path::Tiny;
  13         28  
  13         598  
13 13     13   75 use URI::NamespaceMap;
  13         26  
  13         372  
14 13     13   5318 use Test::FITesque::Test;
  13         9022  
  13         364  
15 13     13   77 use Types::Standard qw(InstanceOf);
  13         32  
  13         110  
16 13     13   6447 use Types::Namespace qw(Iri Namespace);
  13         77  
  13         145  
17 13     13   5005 use Types::Path::Tiny qw(Path);
  13         23  
  13         118  
18 13     13   4650 use Carp qw(carp croak);
  13         24  
  13         563  
19 13     13   70 use Data::Dumper;
  13         55  
  13         459  
20 13     13   118 use HTTP::Request;
  13         24  
  13         347  
21 13     13   58 use HTTP::Response;
  13         23  
  13         243  
22 13     13   53 use LWP::UserAgent;
  13         29  
  13         297  
23 13     13   77 use Try::Tiny;
  13         35  
  13         688  
24 13     13   6932 use Attean::SimpleQueryEvaluator;
  13         346027  
  13         18845  
25              
26             has source => (
27             is => 'ro',
28             isa => Path, # TODO: Generalize to URLs
29             required => 1,
30             coerce => 1,
31             );
32              
33              
34             has base_uri => (
35             is => 'ro',
36             isa => Iri,
37             coerce => 1,
38             default => sub { 'http://localhost/' }
39             );
40              
41             has suite => (
42             is => 'lazy',
43             isa => InstanceOf['Test::FITesque::Suite'],
44             );
45              
46             sub _build_suite {
47 5     5   5352 my $self = shift;
48 5         41 my $suite = Test::FITesque::Suite->new();
49 5         46 foreach my $test (@{$self->transform_rdf}) {
  5         17  
50 6         7550 $suite->add(Test::FITesque::Test->new({ data => $test}));
51             }
52 3         111 return $suite;
53             }
54              
55              
56              
57             sub transform_rdf {
58 17     17 1 80118 my $self = shift;
59 17         299 my $ns = URI::NamespaceMap->new(['deps', 'dc', 'rdf']);
60 17         537585 $ns->add_mapping(test => 'http://ontologi.es/doap-tests#');
61 17         9944 $ns->add_mapping(http => 'http://www.w3.org/2007/ont/http#');
62 17         9536 $ns->add_mapping(httph => 'http://www.w3.org/2007/ont/httph#');
63 17         9758 $ns->add_mapping(dqm => 'http://purl.org/dqm-vocabulary/v1/dqm#');
64 17         9592 $ns->add_mapping(nfo => 'http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#');
65 17         10546 my $parser = Attean->get_parser(filename => $self->source)->new( base => $self->base_uri );
66 17         4154922 my $model = Attean->temporary_model;
67              
68 17         508623 my $graph_id = iri('http://example.org/graph'); # TODO: Use a proper URI for graph
69              
70 17         5691 my $file_iter;
71             try {
72 17     17   1499 $file_iter = $parser->parse_iter_from_io( $self->source->openr_utf8 );
73             } catch {
74 1     1   7912 croak 'Failed to parse ' . $self->source . " due to $_";
75 17         229 };
76 16         1699300 $model->add_iter($file_iter->as_quads($graph_id));
77              
78 16         1085486 my $tests_uri_iter = $model->objects(undef, iri($ns->test->fixtures->as_string))->materialize; # TODO: Implement coercions in Attean
79 16 100       124669 if (scalar $tests_uri_iter->elements == 0) {
80 1         95 croak "No tests found in " . $self->source;
81             }
82              
83 15 100       2696 if ($model->holds($tests_uri_iter->peek, iri($ns->rdf->first->as_string), undef, $graph_id)) {
84             # Then, the object is a list. This supports either unordered
85             # objects or lists, not both. This could be changed by iterating
86             # in the below loop, but I don't see much point to it.
87 6         14469 $tests_uri_iter = $model->get_list( $graph_id, $tests_uri_iter->peek);
88             }
89 15         57195 my @data;
90              
91 15         95 while (my $test_uri = $tests_uri_iter->next) {
92 22         1536 my @instance;
93 22         208 my $params_base_term = $model->objects($test_uri, iri($ns->test->param_base->as_string))->next;
94 22         70638 my $params_base;
95 22 100       89 if ($params_base_term) {
96 10         442 $params_base = URI::Namespace->new($params_base_term);
97 10         1909 $ns->guess_and_add($params_base);
98             }
99 22         406397 my $test_bgp = bgp(triplepattern($test_uri, iri($ns->test->test_script->as_string), variable('script_class')),
100             triplepattern(variable('script_class'), iri($ns->deps->iri('test-requirement')->as_string), variable('handler')), # Because Perl doesn't support dashes in method names
101             triplepattern(variable('script_class'), iri($ns->nfo->definesFunction->as_string), variable('method')),
102             triplepattern($test_uri, iri($ns->test->purpose->as_string), variable('description')),
103             triplepattern($test_uri, iri($ns->test->params->as_string), variable('paramid')));
104              
105 22         153787 my $e = Attean::SimpleQueryEvaluator->new( model => $model, default_graph => $graph_id, ground_blanks => 1 );
106 22         74082 my $test_iter = $e->evaluate( $test_bgp, $graph_id); # Each row will correspond to one test
107              
108 22         522615 while (my $test = $test_iter->next) {
109 20         8586 push(@instance, [$test->value('handler')->value]);
110 20         197 my $method = $test->value('method')->value;
111 20         153 my $params_iter = $model->get_quads($test->value('paramid')); # Get the parameters for each test
112 20         17365 my $params;
113 20         87 $params->{'-special'} = {description => $test->value('description')->value}; # Description should always be present
114 20         265 while (my $param = $params_iter->next) {
115             # First, see if there are HTTP request-responses that can be constructed
116 28         2653 my $pairs_head = $model->objects($param->subject, iri($ns->test->steps->as_string))->next;
117 28         73044 my @pairs;
118              
119 28 100       144 if ($pairs_head) {
120             # There exists a list of HTTP requests and responses
121 10         54 my $steps_iter = $model->get_list($graph_id, $pairs_head);
122 10         68772 while (my $pairs_subject = $steps_iter->next) {
123 15         1064 my $pairs_bgp = bgp(triplepattern($pairs_subject, iri($ns->test->request->as_string), variable('request')),
124             triplepattern($pairs_subject, iri($ns->test->response_assertion->as_string), variable('response_assertion')));
125 15         23690 my $pair_iter = $e->evaluate( $pairs_bgp, $graph_id); # Each row will correspond to one request-response pair
126 15         107361 my $result;
127             # Within each pair, there will be both requests and responses
128 15         139 my $req = HTTP::Request->new;
129 15         1116 my $res = HTTP::Response->new;
130 15         717 my $regex_headers = {};
131 15         54 while (my $pair = $pair_iter->next) {
132             # First, do requests
133 15         4500 my $req_entry_iter = $model->get_quads($pair->value('request'));
134 15         13103 while (my $req_data = $req_entry_iter->next) {
135 52         16883 my $local_header = $ns->httph->local_part($req_data->predicate);
136 52 100       8741 if ($req_data->predicate->equals($ns->http->method)) {
    100          
    100          
    100          
137 13         2755 $req->method($req_data->object->value);
138             } elsif ($req_data->predicate->equals($ns->http->requestURI)) {
139 11         5004 $req->uri($req_data->object->as_string);
140             } elsif ($req_data->predicate->equals($ns->http->content)) {
141 7 100       4672 if ($req_data->object->is_literal) {
    100          
142 4         116 $req->content($req_data->object->value); # TODO: might need encoding
143             } elsif ($req_data->object->is_iri) {
144             # If the http:content predicate points to a IRI, the framework will retrieve content from there
145 2         105 my $ua = LWP::UserAgent->new;
146 2         591 my $content_response = $ua->get(URI->new($req_data->object->as_string));
147 2 100       156779 if ($content_response->is_success) {
148 1         26 $req->content($content_response->decoded_content); # TODO: might need encoding
149             } else {
150 1         26 croak "Could not retrieve content from " . $req_data->object->as_string . " . Got " . $content_response->status_line;
151             }
152             } else {
153 1         48 croak 'Unsupported object ' . $req_data->object->as_string . " in " . $self->source;
154             }
155             } elsif (defined($local_header)) {
156 7         4302 $req->push_header(_find_header($local_header) => $req_data->object->value);
157             }
158             }
159              
160             # Now, do asserted responses
161 13         2479 my $res_entry_iter = $model->get_quads($pair->value('response_assertion'));
162 13         10811 while (my $res_data = $res_entry_iter->next) {
163 40         7995 my $local_header = $ns->httph->local_part($res_data->predicate);
164 40 100       6133 if ($res_data->predicate->equals($ns->http->status)) {
    100          
165 8         1736 $res->code($res_data->object->value);
166             } elsif (defined($local_header)) {
167 19         4270 my $cleaned_header = _find_header($local_header);
168 19         156 $res->push_header($cleaned_header => $res_data->object->value);
169 19 100 100     839 if ($res_data->object->is_literal && $res_data->object->datatype->as_string eq $ns->dqm->regex->as_string) { # TODO: don't use string comparison when Attean does the coercion
170 3         816 $regex_headers->{$cleaned_header} = 1;
171             }
172             }
173             }
174             }
175 13         3086 $result = { 'request' => $req,
176             'response' => $res,
177             'regex-fields' => $regex_headers };
178            
179 13         329 push(@pairs, $result);
180             }
181 8         284 $params->{'-special'}->{'http-pairs'} = \@pairs;
182             }
183 26 100 100     243 if ($param->object->is_literal || $param->object->is_iri) {
184 19         478 my $key = $param->predicate->as_string;
185 19 100 100     1752 if (defined($params_base) && $params_base->local_part($param->predicate)) {
186 16         2745 $key = $params_base->local_part($param->predicate)
187             }
188 19         2796 my $value = $param->object->value;
189 19 100       94 if ($param->object->is_iri) {
190 1         22 $value = URI->new($param->object->as_string)
191             }
192 19         717 $params->{$key} = $value;
193             }
194             }
195 18         1074 push(@instance, [$method, $params])
196             }
197 20 100       986 carp 'Test was listed as ' . $test_uri->as_string . ' but not fully described' unless scalar @instance;
198 20         1990 push(@data, \@instance);
199             }
200 13         942 return \@data;
201             }
202              
203             sub _find_header {
204 26     26   53 my $local_header = shift;
205 26         79 $local_header =~ s/_/-/g; # Some heuristics for creating HTTP headers
206 26         230 $local_header =~ s/\b(\w)/\u$1/g;
207 26         147 return $local_header;
208             }
209              
210             1;
211              
212             __END__
213              
214             =pod
215              
216             =encoding utf-8
217              
218             =head1 NAME
219              
220             Test::FITesque::RDF - Formulate Test::FITesque fixture tables in RDF
221              
222             =head1 SYNOPSIS
223              
224             my $suite = Test::FITesque::RDF->new(source => $file)->suite;
225             $suite->run_tests;
226              
227             See C<t/integration-basic.t> for a full test script example.
228              
229              
230             =head1 DESCRIPTION
231              
232             This module enables the use of Resource Description Framework to
233             describe fixture tables. It will take the filename of an RDF file and
234             return a L<Test::FITesque::Suite> object that can be used to run
235             tests.
236              
237             The RDF serves to identify the implementation of certain fixtures, and
238             can also supply parameters that can be used by the tests, e.g. input
239             parameters or expectations. See L<Test::FITesque> for more on how the
240             fixtures are implemented.
241              
242             =head2 ATTRIBUTES AND METHODS
243              
244             This module implements the following attributes and methods:
245              
246             =over
247              
248             =item C<< source >>
249              
250             Required attribute to the constructor. Takes a L<Path::Tiny> object
251             pointing to the RDF file containing the fixture tables. The value will
252             be converted into an appropriate object, so a string can also be
253             supplied.
254              
255             =item C<< suite >>
256              
257             Will return a L<Test::FITesque::Suite> object, based on the RDF data supplied to the constructor.
258              
259             =item C<< transform_rdf >>
260              
261             Will return an arrayref containing tests in the structure used by
262             L<Test::FITesque::Test>. Most users will rather call the C<suite>
263             method than to call this method directly.
264              
265             =item C<< base_uri >>
266              
267             A L<IRI> to use in parsing the RDF fixture tables to resolve any relative URIs.
268              
269             =back
270              
271             =head2 REQUIRED RDF
272              
273             The following must exist in the test description (see below for an example and prefix expansions):
274              
275             =over
276              
277             =item C<< test:fixtures >>
278              
279             The object(s) of this predicate lists the test fixtures that will run
280             for this test suite. May take an RDF List. Links to the test
281             descriptions, which follow below.
282              
283              
284             =item C<< test:test_script >>
285              
286             The object of this predicate points to information on how the actual
287             test will be run. That is formulated in a separate resource which
288             requires two predicates, C<< deps:test-requirement >> predicate, whose
289             object contains the class name of the implementation of the tests; and
290             C<< nfo:definesFunction >> whose object is a string which matches the
291             actual function name within that class.
292              
293             =item C<< test:purpose >>
294              
295             The object of this predicate provides a literal description of the test.
296              
297             =item C<< test:params >>
298              
299             The object of this predicate links to the parameters, which may have
300             many different shapes. See below for examples.
301              
302             =back
303              
304             =head2 PARAMETERIZATION
305              
306             This module seeks to parameterize the tests, and does so using mostly
307             the C<test:params> predicate above. This is passed on as a hashref to
308             the test scripts.
309              
310             There are two main ways currently implemented, one creates key-value
311             pairs, and uses predicates and objects for that respectively, in
312             vocabularies chosen by the test writer. The other main way is create
313             lists of HTTP requests and responses.
314              
315             If the object of a test parameter is a literal, it will be passed as a
316             plain string, if it is a L<Attean::IRI>, it will be passed as a L<URI>
317             object.
318              
319             Additionally, a special parameter C<-special> is passed on for
320             internal framework use. The leading dash is not allowed as the start
321             character of a local name, and therefore chosen to avoid conflicts
322             with other parameters.
323              
324             The literal given in C<test:purpose> above is passed on as with the
325             C<description> key in this hashref.
326              
327             =head2 RDF EXAMPLE
328              
329             The below example starts with prefix declarations. Then, the
330             tests in the fixture table are listed explicitly. Only tests mentioned
331             using the C<test:fixtures> predicate will be used. Tests may be an RDF
332             List, in which case, the tests will run in the specified sequence, if
333             not, no sequence may be assumed.
334              
335             Then, two test fixtures are declared. The actual implementation is
336             referenced through C<test:test_script> for both functions.
337              
338             The C<test:params> predicate is used to link the parameters that will
339             be sent as a hashref into the function. The <test:purpose> predicate
340             is required to exist outside of the parameters, but will be included
341             as a parameter as well, named C<description> in the C<-special>
342             hashref.
343              
344             There are two mechanisms for passing parameters to the test scripts,
345             one is simply to pass arbitrary key-value pairs, the other is to pass
346             lists of HTTP request-response objects. Both mechanisms may be used.
347              
348             =head3 Key-value parameters
349              
350             The key of the hashref passed as arguments will be the local part of
351             the predicate used in the description (i.e. the part after the colon
352             in e.g. C<my:all>). It is up to the test writer to mint the URIs of
353             the parameters.
354              
355             The test writer may optionally use a C<param_base> to indicate the
356             namespace, in which case the the local part is resolved by the
357             framework, using L<URI::NamespaceMap>. If C<param_base> is not given,
358             the full URI will be passed to the test script.
359              
360              
361             @prefix test: <http://ontologi.es/doap-tests#> .
362             @prefix deps: <http://ontologi.es/doap-deps#>.
363             @prefix dc: <http://purl.org/dc/terms/> .
364             @prefix my: <http://example.org/my-parameters#> .
365             @prefix nfo: <http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#> .
366             @prefix : <http://example.org/test#> .
367              
368              
369             :test_list a test:FixtureTable ;
370             test:fixtures :test1, :test2 .
371              
372             :test1 a test:AutomatedTest ;
373             test:param_base <http://example.org/my-parameters#> ;
374             test:purpose "Echo a string"@en ;
375             test:test_script <http://example.org/simple#string_found> ;
376             test:params [ my:all "counter-clockwise dahut" ] .
377              
378             :test2 a test:AutomatedTest ;
379             test:param_base <http://example.org/my-parameters#> ;
380             test:purpose "Multiply two numbers"@en ;
381             test:test_script <http://example.org/multi#multiplication> ;
382             test:params [
383             my:factor1 6 ;
384             my:factor2 7 ;
385             my:product 42
386             ] .
387              
388             <http://example.org/simple#string_found> a nfo:SoftwareItem ;
389             nfo:definesFunction "string_found" ;
390             deps:test-requirement "Internal::Fixture::Simple"^^deps:CpanId .
391              
392             <http://example.org/multi#multiplication> a nfo:SoftwareItem ;
393             nfo:definesFunction "multiplication" ;
394             deps:test-requirement "Internal::Fixture::Multi"^^deps:CpanId .
395              
396              
397              
398             =head3 HTTP request-response lists
399              
400             To allow testing HTTP-based interfaces, this module also allows the
401             construction of an ordered list of HTTP requests and response pairs.
402             With those, the framework will construct L<HTTP::Request> and
403             L<HTTP::Response> objects. In tests scripts, the request
404             objects will typically be passed to the L<LWP::UserAgent> as input,
405             and then the response from the remote server will be compared with the
406             asserted L<HTTP::Response>s made by the test fixture.
407              
408             We will go through an example in chunks:
409              
410             @prefix test: <http://ontologi.es/doap-tests#> .
411             @prefix deps: <http://ontologi.es/doap-deps#>.
412             @prefix httph:<http://www.w3.org/2007/ont/httph#> .
413             @prefix http: <http://www.w3.org/2007/ont/http#> .
414             @prefix nfo: <http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#> .
415             @prefix : <http://example.org/test#> .
416              
417             :test_list a test:FixtureTable ;
418             test:fixtures :public_writeread_unauthn_alt .
419              
420             :public_writeread_unauthn_alt a test:AutomatedTest ;
421             test:purpose "To test if we can write first using HTTP PUT then read with GET"@en ;
422             test:test_script <http://example.org/httplist#http_req_res_list_unauthenticated> ;
423             test:params [
424             test:steps (
425             [
426             test:request :public_writeread_unauthn_alt_put_req ;
427             test:response_assertion :public_writeread_unauthn_alt_put_res
428             ]
429             [
430             test:request :public_writeread_unauthn_alt_get_req ;
431             test:response_assertion :public_writeread_unauthn_alt_get_res
432             ]
433             )
434             ] .
435              
436             <http://example.org/httplist#http_req_res_list_unauthenticated> a nfo:SoftwareItem ;
437             deps:test-requirement "Example::Fixture::HTTPList"^^deps:CpanId ;
438             nfo:definesFunction "http_req_res_list_unauthenticated" .
439              
440              
441              
442             In the above, after the prefixes, a single test is declared using the
443             C<test:fixtures> predicate, linking to a description of the test. The
444             test is then described as an <test:AutomatedTest>, and it's purpose is
445             declared. It then links to its concrete implementation, which is given
446             in the last three triples in the above.
447              
448             Then, the parameterization is started. In this example, there are two
449             HTTP request-response pairs, which are given as a list object to the
450             C<test:steps> predicate.
451              
452             To link the request, the C<test:request> predicate is used, to link
453             the asserted response, the C<test:response_assertion> predicate is
454             used.
455              
456             Next, we look into the actual request and response messages linked from the above:
457              
458             :public_writeread_unauthn_alt_put_req a http:RequestMessage ;
459             http:method "PUT" ;
460             httph:content_type "text/turtle" ;
461             http:content "</public/foobar.ttl#dahut> a <http://example.org/Cryptid> ." ;
462             http:requestURI </public/foobar.ttl> .
463              
464             :public_writeread_unauthn_alt_put_res a http:ResponseMessage ;
465             http:status 201 .
466              
467             :public_writeread_unauthn_alt_get_req a http:RequestMessage ;
468             http:method "GET" ;
469             http:requestURI </public/foobar.ttl> .
470              
471             :public_writeread_unauthn_alt_get_res a http:ResponseMessage ;
472             httph:accept_post "text/turtle", "application/ld+json" ;
473             httph:content_type "text/turtle" .
474              
475             These should be self-explanatory, but note that headers are given with
476             lower-case names and underscores. They will be transformed to headers
477             by replacing underscores with dashes and upcase the first letters.
478              
479             This module will transform the above to data structures that are
480             suitable to be passed to L<Test::Fitesque>, and the above will appear as
481              
482             {
483             '-special' => {
484             'http-pairs' => [
485             {
486             'request' => ... ,
487             'response' => ... ,
488             },
489             { ... }
490             ]
491             },
492             'description' => 'To test if we can write first using HTTP PUT then read with GET'
493             },
494             }
495              
496              
497             Note that there are more examples in this module's test suite in the
498             C<t/data/> directory.
499              
500             You may maintain client state in a test script (i.e. for one
501             C<test:AutomatedTest>, as it is simply one script, so the result of
502             one request may be used to influence the next. Server state can be
503             relied on between different tests by using an C<rdf:List> of test
504             fixtures if it writes something into the server, there is nothing in
505             the framework that changes that.
506              
507             To use data from one response to influence subsequent requests, the
508             framework supports datatyping literals with the C<dqm:regex> datatype,
509             for example:
510              
511             :check_acl_location_res a http:ResponseMessage ;
512             httph:link '<(.*?)>;\\s+rel="acl"'^^dqm:regex ;
513             http:status 200 .
514              
515             This makes it possible to use a Perl regular expression, which can be
516             executed in a test script if desired. If present, it will supply
517             another hashref to the C<http-pairs> key with the key C<regex-fields>
518             containing hashrefs with the header field that had a correspondiing
519             object datatyped regex as key and simply C<1> as value.
520              
521             =head1 TODO
522              
523             Separate the implementation-specific details (such as C<deps:test-requirement>)
524             from the actual fixture tables.
525              
526             =head1 BUGS
527              
528             Please report any bugs to
529             L<https://github.com/kjetilk/p5-test-fitesque-rdf/issues>.
530              
531             =head1 SEE ALSO
532              
533             =head1 AUTHOR
534              
535             Kjetil Kjernsmo E<lt>kjetilk@cpan.orgE<gt>.
536              
537             =head1 COPYRIGHT AND LICENCE
538              
539             This software is Copyright (c) 2019 by Inrupt Inc.
540              
541             This is free software, licensed under:
542              
543             The MIT (X11) License
544              
545              
546             =head1 DISCLAIMER OF WARRANTIES
547              
548             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
549             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
550             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
551