View | Details | Raw Unified | Return to bug 14748
Collapse All | Expand All

(-)a/t/db_dependent/Api/V1/Borrowers.pm (+69 lines)
Line 0 Link Here
1
package t::db_dependent::Api::V1::Borrowers;
2
3
use Modern::Perl;
4
use Test::More;
5
6
use t::lib::TestObjects::BorrowerFactory;
7
use t::lib::TestObjects::ObjectFactory;
8
9
#GET /borrowers/{borrowernumber}, with response 200
10
11
sub get_n_200 {
12
    my ($class, $restTest, $driver) = @_;
13
    my $testContext = $restTest->get_testContext(); #Test context will be automatically cleaned after this subtest has been executed.
14
    my $activeUser = $restTest->get_activeBorrower();
15
16
    #Create the test borrower.
17
    my $b = [{ cardnumber => '11A01', firstname => 'Olli', surname => 'Kiivi', password => '1234', userid => 'admin', branchcode => 'CPL' }];
18
    my $borrowers = t::lib::TestObjects::BorrowerFactory->createTestGroup($b, undef, $testContext, undef, undef);
19
    $b = $borrowers->{'11A01'};
20
    my $borrowernumber = $b->borrowernumber;
21
22
    #Execute request
23
    my $path = $restTest->get_routePath();
24
    $path =~ s/\{borrowernumber\}/$borrowernumber/;
25
    $driver->get_ok($path => {Accept => 'text/json'});
26
    $driver->status_is(200);
27
28
    #Compare result
29
    $driver->json_is('/borrowernumber' => $borrowernumber, "Got the same borrower '$borrowernumber'");
30
    my $json = $driver->tx->res->json();
31
    my $body = $driver->tx->res->body();
32
33
    return 1;
34
}
35
36
sub get_n_404 {
37
    my ($class, $restTest, $driver) = @_;
38
    my $testContext = $restTest->get_testContext(); #Test context will be automatically cleaned after this subtest has been executed.
39
    my $activeUser = $restTest->get_activeBorrower();
40
41
    #Create the test borrower and get the borrowernumber. We know for sure that this borrowernumber is not already in use.
42
    my $b = [{ cardnumber => '11A01', firstname => 'Olli', surname => 'Kiivi', password => '1234', userid => 'admin', branchcode => 'CPL' }];
43
    my $borrowers = t::lib::TestObjects::BorrowerFactory->createTestGroup($b, undef, $testContext, undef, undef);
44
    $b = $borrowers->{'11A01'};
45
    my $borrowernumber = $b->borrowernumber;
46
47
    #Delete the borrower so we can no longer find it.
48
    t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext);
49
50
    #Make the HTTP request
51
    my $path = $restTest->get_routePath();
52
    $path =~ s/\{borrowernumber\}/$borrowernumber/;
53
    $driver->get_ok($path => {Accept => 'text/json'});
54
    $driver->status_is(404);
55
56
    return 1;
57
}
58
59
sub get200 {
60
    my ($class, $restTest, $driver) = @_;
61
62
    #Make the HTTP request
63
    my $path = $restTest->get_routePath();
64
    $driver->get_ok($path => {Accept => 'text/json'});
65
    $driver->status_is(200);
66
    $driver->json_has('email', 'Got something meaningful');
67
}
68
69
1;
(-)a/t/db_dependent/Api/V1/testREST.pl (+37 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
use Test::More;
5
use Getopt::Long;
6
7
use t::lib::Swagger2TestRunner;
8
9
# These are defaults for command line options.
10
my @testKeywords;
11
my $help    = 0;
12
13
GetOptions(
14
            'h|help'          => \$help,
15
            'k|keywords=s{,}' => \@testKeywords,
16
       );
17
18
my $helpText = <<HELP;
19
20
perl t/db_dependent/Api/V1/testREST.pl
21
22
Triggers all tests for all REST API endpoints for V1.
23
See. t::lib::Swagger2TestRunner->new() for parameter descriptions.
24
25
  --help        This help text.
26
27
HELP
28
29
30
if ($help) {
31
    print $helpText;
32
    exit;
33
}
34
35
36
my $testRunner = t::lib::Swagger2TestRunner->new({testKeywords => \@testKeywords});
37
$testRunner->testREST();
(-)a/t/lib/RESTTest.pm (+133 lines)
Line 0 Link Here
1
package t::lib::RESTTest;
2
3
use Modern::Perl;
4
use Test::More;
5
6
use Class::Accessor "antlers";
7
__PACKAGE__->follow_best_practice();
8
9
has testImplementationMainPackage => (is => 'rw', isa => 'Str'); #eg. 't::db_dependent::api'
10
has basePath        => (is => 'rw', isa => 'Str'); #eg. '/v1'
11
has pathsObjectPath => (is => 'rw', isa => 'Str'); #eg. '/borrowers/{borrowernumber}/issues/{itemnumber}'
12
has httpVerb        => (is => 'rw', isa => 'Str'); #eg. 'get'
13
has httpStatusCode  => (is => 'rw', isa => 'Str'); #eg. '404'
14
has packageName     => (is => 'rw', isa => 'Str'); #eg. 'Borrowers'
15
has subroutineName  => (is => 'rw', isa => 'Str'); #eg. 'get_n_200'
16
has swagger2specification => (is => 'rw', isa => 'HASH'); #The Swagger2 specification as HASH
17
has operationObject => (is => 'rw', isa => 'HASH'); #The Swagger2 specifications OperationObject this test tests.
18
has activeBorrower  => (is => 'rw', isa => 'Koha::Borrower'); #The Borrower who is consuming the REST API, and doing the authentication.
19
has apiKey          => (is => 'rw', isa => 'Str'); #eg."23f08sev90-42vfwv+ave3v==Ac", active borrower's api key
20
has testContext     => (is => 'rw', isa => 'HASH'); #Test context for this test case, used to collect all DB modifications in one place for easy removal.
21
22
use Koha::Exception::BadParameter;
23
24
=head new
25
26
    t::lib::RESTTest->new({
27
            testImplementationMainPackage => 't::db_dependent::api',
28
            basePath => '/v1',
29
            pathsObj_Path => '/borrowers/{borrowernumber}/issues/{itemnumber}',
30
            httpVerb => 'get',
31
            httpStatusCode => '200',
32
            specification => $specification,
33
            operationObject => $operationObject,
34
            activeBorrower => $activeBorrower,
35
            apiKey => "23f08sev90-42vfwv+ave3v==Ac", #Active borrowers api key
36
    });
37
38
=cut
39
40
our $testImplementationMainPackage = 't::db_dependent::Api';
41
42
sub new {
43
    my ($class, $params) = @_;
44
45
    bless($params, $class);
46
    _validateParams($params);
47
48
    $params->set_testImplementationMainPackage($testImplementationMainPackage) unless $params->get_testImplementationMainPackage();
49
    $params->set_testContext({});
50
51
    #We need to create the package and subroutine path to dynamically check that test-subroutines are defined for the specified paths.
52
    $params->_buildPackageAndSubroutineName();
53
    return $params;
54
}
55
56
=head _validateParams
57
58
@THROWS Koha::Exception::BadParameter, if validation fails.
59
=cut
60
61
sub _validateParams {
62
    my ($params) = @_;
63
64
    ##Actually check the params
65
    unless ($params->{basePath}) {
66
        $params->{basePath} = 'v1';
67
    }
68
69
    if ($params->{pathsObjectPath}) {
70
        my ($sModule, $sPathTail) = ($1, $2) if $params->{pathsObjectPath} =~ /^\/(\w+)\/?(.*)$/;
71
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> Unable to parse the module name from '".$params->{pathsObjectPath}."'. Please fix the module parser!")
72
                    unless $sModule;
73
    }
74
    else {
75
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> no 'pathsObjectPath'-parameter given!");
76
    }
77
78
    unless ($params->{httpVerb}) {
79
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> no 'httpVerb'-parameter given!");
80
    }
81
82
    unless ($params->{httpStatusCode}) {
83
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> no 'httpStatusCode'-parameter given!");
84
    }
85
86
    unless ($params->{swagger2specification}) {
87
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> no 'swagger2specification'-parameter given!");
88
    }
89
90
    unless ($params->{operationObject}) {
91
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> no 'operationObject'-parameter given!");
92
    }
93
94
    unless ($params->{activeBorrower}) {
95
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> no 'activeBorrower'-parameter given!");
96
    }
97
98
    unless ($params->{apiKey}) {
99
        Koha::Exception::BadParameter->throw(error => "RESTTest->new():> no 'apiKey'-parameter given!");
100
    }
101
}
102
103
sub _buildPackageAndSubroutineName {
104
    my ($self) = @_;
105
106
    my $bp = $self->get_basePath();
107
    $bp =~ s/\///g;
108
109
    my ($sModule, $sPathTail) = ($1, $2) if $self->get_pathsObjectPath() =~ /^\/(\w+)\/?(.*)$/;
110
    $sPathTail =~ s/\{.*?\}/_n_/g;
111
    $sPathTail =~ s/\///g;
112
113
    my $testPackageName = $testImplementationMainPackage.'::'. #typically t::db_dependent::api
114
                             ucfirst($bp).'::'. #version, v1
115
                             ucfirst($sModule); #borrowers, etc.
116
    $self->set_packageName($testPackageName);
117
118
    my $testSubroutineName = $self->get_httpVerb(). #eg. get
119
                             (($sPathTail) ? $sPathTail : ''). #eg. borrowers_n_
120
                             $self->get_httpStatusCode(); #eg. 200, 404, 403, ...
121
    $self->set_subroutineName($testSubroutineName);
122
}
123
124
sub get_routePath {
125
    my $self = shift;
126
    return $self->get_basePath().$self->get_pathsObjectPath();
127
}
128
sub get_requiredPermissions {
129
    my ($self) = @_;
130
    return $self->get_operationObject()->{'x-koha-permission'};
131
}
132
133
1;
(-)a/t/lib/Swagger2TestRunner.pm (-1 / +282 lines)
Line 0 Link Here
0
- 
1
package t::lib::Swagger2TestRunner;
2
3
use Modern::Perl;
4
use Test::More;
5
6
use Swagger2;
7
use YAML::XS;
8
use String::Random;
9
10
use C4::Context;
11
12
use Koha::ApiKeys;
13
use Koha::Auth::PermissionManager;
14
use Koha::Auth::Permissions;
15
use Koha::Auth::Challenge::RESTV1;
16
17
use t::lib::WebDriverFactory;
18
use t::lib::RESTTest;
19
use t::lib::TestObjects::BorrowerFactory;
20
use t::lib::TestObjects::ObjectFactory;
21
22
use Koha::Exception::FeatureUnavailable;
23
24
=head new
25
26
    my $swagger2TestRunner = t::lib::Swagger2TestRunner->new({
27
                                                            testKeywords => ['borrowers','^issues', '@/borrowers/{borrowernumber}'],
28
    });
29
30
Gets the testrunner for you to operate on the REST API tests.
31
@PARAM1 HASHRef of parameters: {
32
            #Every route in the Swagger2 interface definition is matched against these
33
            #given keywords. Matching routes are targeted for operations.
34
            #    Keywords starting with ^ are negated, so they must not match
35
            #    Keywords starting with @ are complete matches, so they must match the url endpoint completely. A bit like a tag.
36
            #    Keywords with no special starting character must be found from the endpoint/url.
37
            #The special starting character is removed before matching.
38
            testKeywords => ['borrowers','^issues', '@/borrowers/{borrowernumber}'],
39
        }
40
=cut
41
42
sub new {
43
    my ($class, $params) = @_;
44
45
    my $self = $params || {};
46
    bless($self, $class);
47
48
    my $swagger = Swagger2->new->load( _getSwagger2Syspref() )->expand;
49
    $self->{swagger} = $swagger;
50
    $self->{testContext} = {}; #Collect all objects created for testing here for easy test context tearDown.
51
52
    return $self;
53
}
54
55
=head testREST
56
Gets and runs all the tests for all endpoints.
57
=cut
58
59
sub testREST {
60
    my ($self) = @_;
61
    my $tests = $self->getTests();
62
    $self->runTests($tests);
63
}
64
65
=head getTests
66
Reads through the Swagger2 API definition and prepares tests for each
67
returned HTTP status code within
68
each accepted HTTP verb within
69
each defined API endpoint/url
70
71
This enforces that no documented case is left untested.
72
If the 'testKeywords'-parameter is given during object instantiation, filters endpoints
73
with the keywords.
74
@RETURNS ARRAYRef of RESTTest-objects.
75
@THROW Koha::Exception::FeatureUnavailable, if Swagger2 is misconfigured or missing.
76
=cut
77
78
sub getTests {
79
    my ($self) = @_;
80
81
    my $tests = [];
82
    my $specification = $self->{swagger}->{tree}->{data};
83
    my $pathsObject = $specification->{paths};
84
    my $basePath = $specification->{basePath};
85
86
    my $activeBorrower = _getTestBorrower($self->{testContext}); #The Borrower who authenticates for the tests
87
    my $apiKey = Koha::ApiKeys->grant($activeBorrower);
88
89
    #Find the response object which encompasses all documented permutations of API calls.
90
    foreach my $pathsObject_path (grep {$_ if $_ !~ /^x-/i } sort keys(%$pathsObject)) { #Pick only Path Item Objects
91
        next unless _testPathsObject_pathAgainstTestKeywords($pathsObject_path, $self->{testKeywords});
92
93
        my $pathItemObject = $pathsObject->{$pathsObject_path};
94
        foreach my $httpVerb (grep {$_ if $_ =~ /(get|put|post|delete|options|head|patch)/i} sort keys(%$pathItemObject)) { #Pick only Operation Objects
95
            my $operationObject = $pathItemObject->{$httpVerb};
96
            foreach my $httpStatusCode (grep {$_ if $_ !~ /^x-/i } sort keys(%{$operationObject->{responses}})) { #Pick only Response Objects from the Responses Object.
97
                my $responseObject = $operationObject->{responses}->{$httpStatusCode};
98
99
                my $subtest = t::lib::RESTTest->new({basePath => $basePath,
100
                                                     pathsObjectPath => $pathsObject_path,
101
                                                     httpVerb => $httpVerb,
102
                                                     httpStatusCode => $httpStatusCode,
103
                                                     swagger2specification => $specification,
104
                                                     operationObject => $operationObject,
105
                                                     activeBorrower => $activeBorrower,
106
                                                     apiKey => $apiKey,
107
                                                     });
108
                push @$tests, $subtest;
109
            }
110
        }
111
    }
112
    @$tests = reverse @$tests;
113
    return $tests;
114
}
115
116
=head runTests
117
Executes the prepared tests.
118
Sets up the necessary authentication prerequisites documented in the Swagger2 API definition
119
prior to executing the test subroutine.
120
Tears down any preconfigured changes after each test.
121
=cut
122
123
sub runTests {
124
    my ($self, $tests) = @_;
125
126
    print "testREST():> Starting testing >>>\n";
127
    my ($driver) = t::lib::WebDriverFactory::getUserAgentDrivers('mojolicious');
128
    foreach my $subtest (@$tests) {
129
        my $testPackageName = $subtest->get_packageName();
130
        my $testSubroutineName = $subtest->get_subroutineName();
131
        eval "require $testPackageName";
132
        if ($@) {
133
            warn "$@\n";
134
        }
135
136
        if ($@) { #Trigger this test to fail if the package is unimplemented
137
            ok(0, "No test package defined for API route '".$subtest->get_routePath()."'. You must define it in $testPackageName->$testSubroutineName().");
138
        }
139
        elsif (not("$testPackageName"->can("$testSubroutineName"))) {
140
            ok(0, "No test subroutine defined for API route '".$subtest->get_routePath()."'. You must define it in $testPackageName->$testSubroutineName().");
141
        }
142
        else {
143
            $self->_prepareTestContext( $subtest, $driver );
144
145
            eval { #Prevent propagation of death from above, so we can continue testing and clean up the test context afterwards.
146
                no strict 'refs';
147
                subtest "$testPackageName->$testSubroutineName()" => sub {
148
                    "$testPackageName"->$testSubroutineName( $subtest, $driver );
149
                };
150
            };
151
            if ($@) {
152
                warn "$@\n";
153
            }
154
155
            $self->_tearDownTestContext( $subtest, $driver );
156
        }
157
    }
158
    t::lib::TestObjects::ObjectFactory->tearDownTestContext($self->{testContext}); #Clear the global REST test context.
159
    done_testing;
160
}
161
162
sub _getSwagger2Syspref {
163
    my $swagger2DefinitionLocation = $ENV{KOHA_PATH}.'/api/v1/swagger.json';
164
    unless (-f $swagger2DefinitionLocation) {
165
        Koha::Exception::FeatureUnavailable->throw(error => "Swagger2TestRunner():> Couldn't find the Swagger2 definitions file from '$swagger2DefinitionLocation'. You must have a swagger.json-file to use the test runner.");
166
    }
167
    return $swagger2DefinitionLocation;
168
}
169
170
=head _getTestBorrower
171
Gets the universal active test Borrower used in all REST tests as the Borrower consuming the API.
172
=cut
173
174
sub _getTestBorrower {
175
    my ($testContext) = @_;
176
    my $borrowerFactory = t::lib::TestObjects::BorrowerFactory->new();
177
    my $borrowers = $borrowerFactory->createTestGroup([
178
            {firstname  => 'TestRunner',
179
             surname    => 'AI',
180
             cardnumber => '11A000',
181
             branchcode => 'CPL',
182
             address    => 'Technological Singularity',
183
             city       => 'Gehenna',
184
             zipcode    => '80140',
185
             email      => 'bionicman@example.com',
186
             categorycode => 'PT',
187
             dateofbirth => DateTime->now(time_zone => C4::Context->tz())->subtract(years => 21)->iso8601(), #I am always 21!
188
            },
189
        ], undef, $testContext);
190
    return $borrowers->{'11A000'};
191
}
192
193
=head _testPathsObject_pathAgainstTestKeywords
194
Implements the 'testKeywords'-parameter introduced in the constructor '->new()'.
195
=cut
196
197
sub _testPathsObject_pathAgainstTestKeywords {
198
    my ($pathsObject_path, $keywords) = @_;
199
    return 1 unless $keywords;
200
    foreach my $kw (@$keywords) {
201
        my ($exclude, $include, $only);
202
        if ($kw =~ /^\^(.+?)$/) {
203
            $exclude = $1;
204
        }
205
        elsif ($kw =~ /^\@(.+?)$/) {
206
            $only = $1;
207
        }
208
        else {
209
            $include = $kw;
210
        }
211
212
        if ($include) {
213
            return undef unless $pathsObject_path =~ m/\Q$include/;
214
        }
215
        elsif ($exclude) {
216
            return undef if $pathsObject_path =~ m/\Q$exclude/;
217
        }
218
        elsif ($only) {
219
            return undef unless $pathsObject_path =~ m/^\Q$only\E$/;
220
        }
221
    }
222
    return 1;
223
}
224
225
=head _prepareTestContext
226
Help to make implementing these tests maximally easy.
227
Make sure the active test Borrower has proper permissions to access the resource
228
 and the authentication headers are properly set.
229
=cut
230
231
sub _prepareTestContext {
232
    my ($self, $subtest, $driver) = @_;
233
    my $permissionsRequired = _replaceAnyPermissions( $subtest->get_requiredPermissions() );
234
235
    my $permissionManager = Koha::Auth::PermissionManager->new();
236
    $permissionManager->grantPermissions($subtest->get_activeBorrower(), $permissionsRequired);
237
238
    #Prepare authentication headers
239
    $driver->ua->once(start => sub { #Subscribe only once to this event, we need fresh headers on every HTTP request.
240
        my ($ua, $tx) = @_;
241
        my $headers = Koha::Auth::Challenge::RESTV1::prepareAuthenticationHeaders($subtest->get_activeBorrower(), undef, $subtest->get_httpVerb());
242
        $tx->req->headers->add('X-Koha-Date' => $headers->{'X-Koha-Date'});
243
        $tx->req->headers->add('Authorization' => $headers->{Authorization});
244
    });
245
}
246
247
=head _tearDownTestContext
248
Help to make implementing these tests maximally easy.
249
Remove all granted permissions so they wont interfere with other REST tests.
250
251
Also purges the test context created during test execution, if the context has been populated with TestObjectFactories.
252
=cut
253
254
sub _tearDownTestContext {
255
    my ($self, $subtest, $driver) = @_;
256
    my $testContext = $subtest->get_testContext();
257
258
    my $permissionManager = Koha::Auth::PermissionManager->new();
259
    $permissionManager->revokeAllPermissions($subtest->{activeBorrower});
260
261
    t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext);
262
263
    sleep 1; #Wait for Test::Mojo to clean up and reattach event handlers in it's asynchronous internals.
264
}
265
266
=head _replaceAnyPermission
267
When fulfilling the *-permission, we need to find any permission under the given
268
permissionmodule to satisfy the permission requirement.
269
=cut
270
271
sub _replaceAnyPermissions {
272
    my ($permissionsRequired) = @_;
273
    foreach my $module (keys %$permissionsRequired) {
274
        if ( $permissionsRequired->{$module} eq '*' ) {
275
            my @permissions = Koha::Auth::Permissions->search({module => $module});
276
            $permissionsRequired->{$module} = $permissions[0]->code;
277
        }
278
    }
279
    return $permissionsRequired;
280
}
281
282
1;

Return to bug 14748