Bug 16330: Move patches to OpenAPI
[koha.git] / t / db_dependent / api / v1 / patrons.t
1 #!/usr/bin/env perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with Koha; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 use Modern::Perl;
19
20 use Test::More tests => 5;
21 use Test::Mojo;
22
23 use t::lib::TestBuilder;
24 use t::lib::Mocks;
25
26 use C4::Auth;
27 use Koha::Database;
28
29 my $schema  = Koha::Database->new->schema;
30 my $builder = t::lib::TestBuilder->new;
31
32 # FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
33 # this affects the other REST api tests
34 t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
35
36 my $remote_address = '127.0.0.1';
37 my $t              = Test::Mojo->new('Koha::REST::V1');
38
39 subtest 'list() tests' => sub {
40     plan tests => 2;
41
42     $schema->storage->txn_begin;
43
44     unauthorized_access_tests('GET', undef, undef);
45
46     subtest 'librarian access tests' => sub {
47         plan tests => 8;
48
49         my ($borrowernumber, $sessionid) = create_user_and_session({
50             authorized => 1 });
51         my $patron = Koha::Patrons->find($borrowernumber);
52         Koha::Patrons->search({
53             borrowernumber => { '!=' => $borrowernumber},
54             cardnumber => { LIKE => $patron->cardnumber . "%" }
55         })->delete;
56         Koha::Patrons->search({
57             borrowernumber => { '!=' => $borrowernumber},
58             address2 => { LIKE => $patron->address2 . "%" }
59         })->delete;
60
61         my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
62         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
63         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
64         $t->request_ok($tx)
65           ->status_is(200);
66
67         $tx = $t->ua->build_tx(GET => '/api/v1/patrons?cardnumber='.
68                                   $patron->cardnumber);
69         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
70         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
71         $t->request_ok($tx)
72           ->status_is(200)
73           ->json_is('/0/cardnumber' => $patron->cardnumber);
74
75         $tx = $t->ua->build_tx(GET => '/api/v1/patrons?address2='.
76                                   $patron->address2);
77         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
78         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
79         $t->request_ok($tx)
80           ->status_is(200)
81           ->json_is('/0/address2' => $patron->address2);
82     };
83
84     $schema->storage->txn_rollback;
85 };
86
87 subtest 'get() tests' => sub {
88     plan tests => 3;
89
90     $schema->storage->txn_begin;
91
92     unauthorized_access_tests('GET', -1, undef);
93
94     subtest 'access own object tests' => sub {
95         plan tests => 4;
96
97         my ($patronid, $patronsessionid) = create_user_and_session({
98             authorized => 0 });
99
100         # Access patron's own data even though they have no borrowers flag
101         my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$patronid");
102         $tx->req->cookies({name => 'CGISESSID', value => $patronsessionid});
103         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
104         $t->request_ok($tx)
105           ->status_is(200);
106
107         my $guarantee = $builder->build({
108             source => 'Borrower',
109             value  => {
110                 guarantorid => $patronid,
111             }
112         });
113
114         # Access guarantee's data even though guarantor has no borrowers flag
115         my $guaranteenumber = $guarantee->{borrowernumber};
116         $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$guaranteenumber");
117         $tx->req->cookies({name => 'CGISESSID', value => $patronsessionid});
118         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
119         $t->request_ok($tx)
120           ->status_is(200);
121     };
122
123     subtest 'librarian access tests' => sub {
124         plan tests => 5;
125
126         my ($patron_id) = create_user_and_session({
127             authorized => 0 });
128         my $patron = Koha::Patrons->find($patron_id);
129         my ($borrowernumber, $sessionid) = create_user_and_session({
130             authorized => 1 });
131         my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$patron_id");
132         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
133         $t->request_ok($tx)
134           ->status_is(200)
135           ->json_is('/borrowernumber' => $patron_id)
136           ->json_is('/surname' => $patron->surname)
137           ->json_is('/lost' => Mojo::JSON->false );
138     };
139
140     $schema->storage->txn_rollback;
141 };
142
143 subtest 'add() tests' => sub {
144     plan tests => 2;
145
146     $schema->storage->txn_begin;
147
148     my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
149     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
150     my $newpatron = {
151         address      => 'Street',
152         branchcode   => $branchcode,
153         cardnumber   => $branchcode.$categorycode,
154         categorycode => $categorycode,
155         city         => 'Joenzoo',
156         surname      => "TestUser",
157         userid       => $branchcode.$categorycode,
158     };
159
160     unauthorized_access_tests('POST', undef, $newpatron);
161
162     subtest 'librarian access tests' => sub {
163         plan tests => 18;
164
165         my ($borrowernumber, $sessionid) = create_user_and_session({
166             authorized => 1 });
167
168         $newpatron->{branchcode} = "nonexistent"; # Test invalid branchcode
169         my $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron );
170         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
171         $t->request_ok($tx)
172           ->status_is(400)
173           ->json_is('/error' => "Given branchcode does not exist");
174         $newpatron->{branchcode} = $branchcode;
175
176         $newpatron->{categorycode} = "nonexistent"; # Test invalid patron category
177         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
178         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
179         $t->request_ok($tx)
180           ->status_is(400)
181           ->json_is('/error' => "Given categorycode does not exist");
182         $newpatron->{categorycode} = $categorycode;
183
184         $newpatron->{falseproperty} = "Non existent property";
185         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
186         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
187         $t->request_ok($tx)
188           ->status_is(400);
189         delete $newpatron->{falseproperty};
190
191         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
192         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
193         $t->request_ok($tx)
194           ->status_is(201, 'Patron created successfully')
195           ->json_has('/borrowernumber', 'got a borrowernumber')
196           ->json_is('/cardnumber', $newpatron->{ cardnumber })
197           ->json_is('/surname' => $newpatron->{ surname })
198           ->json_is('/firstname' => $newpatron->{ firstname });
199         $newpatron->{borrowernumber} = $tx->res->json->{borrowernumber};
200
201         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
202         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
203         $t->request_ok($tx)
204           ->status_is(409)
205           ->json_has('/error', 'Fails when trying to POST duplicate'.
206                      ' cardnumber or userid')
207           ->json_has('/conflict', {
208                         userid => $newpatron->{ userid },
209                         cardnumber => $newpatron->{ cardnumber }
210                     }
211             );
212     };
213
214     $schema->storage->txn_rollback;
215 };
216
217 subtest 'update() tests' => sub {
218     plan tests => 2;
219
220     $schema->storage->txn_begin;
221
222     unauthorized_access_tests('PUT', 123, {email => 'nobody@example.com'});
223
224     subtest 'librarian access tests' => sub {
225         plan tests => 20;
226
227         t::lib::Mocks::mock_preference('minPasswordLength', 1);
228         my ($borrowernumber, $sessionid) = create_user_and_session({ authorized => 1 });
229         my ($borrowernumber2, undef) = create_user_and_session({ authorized => 0 });
230
231         my $patron_1  = Koha::Patrons->find($borrowernumber);
232         my $patron_2  = Koha::Patrons->find($borrowernumber2);
233         my $newpatron = $patron_2->TO_JSON;
234
235         my $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/-1" => json => $newpatron );
236         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
237         $t->request_ok($tx)
238           ->status_is(404)
239           ->json_has('/error', 'Fails when trying to PUT nonexistent patron');
240
241         $newpatron->{categorycode} = 'nonexistent';
242         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/$borrowernumber2" => json => $newpatron );
243         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
244         $t->request_ok($tx)
245           ->status_is(400)
246           ->json_is('/error' => "Given categorycode does not exist");
247         $newpatron->{categorycode} = $patron_2->categorycode;
248
249         $newpatron->{branchcode} = 'nonexistent';
250         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/$borrowernumber2" => json => $newpatron );
251         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
252         $t->request_ok($tx)
253           ->status_is(400)
254           ->json_is('/error' => "Given branchcode does not exist");
255         $newpatron->{branchcode} = $patron_2->branchcode;
256
257         $newpatron->{falseproperty} = "Non existent property";
258         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/$borrowernumber2" => json => $newpatron );
259         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
260         $t->request_ok($tx)
261           ->status_is(400)
262           ->json_is('/errors/0/message' =>
263                     'Properties not allowed: falseproperty.');
264         delete $newpatron->{falseproperty};
265
266         # Set both cardnumber and userid to already existing values
267         $newpatron->{cardnumber} = $patron_1->cardnumber;
268         $newpatron->{userid}     = $patron_1->userid;
269
270         $tx = $t->ua->build_tx( PUT => "/api/v1/patrons/$borrowernumber2" => json => $newpatron );
271         $tx->req->cookies({ name => 'CGISESSID', value => $sessionid });
272         $t->request_ok($tx)->status_is(409)
273           ->json_has( '/error' => "Fails when trying to update to an existing cardnumber or userid")
274           ->json_is(  '/conflict',
275                         {
276                             cardnumber => $newpatron->{cardnumber},
277                             userid     => $newpatron->{userid}
278                         }
279           );
280
281         $newpatron->{ cardnumber } = $borrowernumber.$borrowernumber2;
282         $newpatron->{ userid } = "user".$borrowernumber.$borrowernumber2;
283         $newpatron->{ surname } = "user".$borrowernumber.$borrowernumber2;
284
285         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
286                     $newpatron->{borrowernumber} => json => $newpatron);
287         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
288         $t->request_ok($tx)
289           ->status_is(200, 'Patron updated successfully')
290           ->json_has($newpatron);
291         is(Koha::Patrons->find($newpatron->{borrowernumber})->cardnumber,
292            $newpatron->{ cardnumber }, 'Patron is really updated!');
293     };
294
295     $schema->storage->txn_rollback;
296 };
297
298 subtest 'delete() tests' => sub {
299     plan tests => 2;
300
301     $schema->storage->txn_begin;
302
303     unauthorized_access_tests('DELETE', 123, undef);
304
305     subtest 'librarian access test' => sub {
306         plan tests => 4;
307
308         my ($borrowernumber, $sessionid) = create_user_and_session({
309             authorized => 1 });
310         my ($borrowernumber2, $sessionid2) = create_user_and_session({
311             authorized => 0 });
312
313         my $tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/-1");
314         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
315         $t->request_ok($tx)
316           ->status_is(404, 'Patron not found');
317
318         $tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/$borrowernumber2");
319         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
320         $t->request_ok($tx)
321           ->status_is(200, 'Patron deleted successfully');
322     };
323
324     $schema->storage->txn_rollback;
325 };
326
327 # Centralized tests for 401s and 403s assuming the endpoint requires
328 # borrowers flag for access
329 sub unauthorized_access_tests {
330     my ($verb, $patronid, $json) = @_;
331
332     my $endpoint = '/api/v1/patrons';
333     $endpoint .= ($patronid) ? "/$patronid" : '';
334
335     subtest 'unauthorized access tests' => sub {
336         plan tests => 5;
337
338         my $tx = $t->ua->build_tx($verb => $endpoint => json => $json);
339         $t->request_ok($tx)
340           ->status_is(401);
341
342         my ($borrowernumber, $sessionid) = create_user_and_session({
343             authorized => 0 });
344
345         $tx = $t->ua->build_tx($verb => $endpoint => json => $json);
346         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
347         $t->request_ok($tx)
348           ->status_is(403)
349           ->json_has('/required_permissions');
350     };
351 }
352
353 sub create_user_and_session {
354
355     my $args  = shift;
356     my $flags = ( $args->{authorized} ) ? 16 : 0;
357
358     my $user = $builder->build(
359         {
360             source => 'Borrower',
361             value  => {
362                 flags => $flags,
363                 gonenoaddress => 0,
364                 lost => 0,
365                 email => 'nobody@example.com',
366                 emailpro => 'nobody@example.com',
367                 B_email => 'nobody@example.com'
368             }
369         }
370     );
371
372     # Create a session for the authorized user
373     my $session = C4::Auth::get_session('');
374     $session->param( 'number',   $user->{borrowernumber} );
375     $session->param( 'id',       $user->{userid} );
376     $session->param( 'ip',       '127.0.0.1' );
377     $session->param( 'lasttime', time() );
378     $session->flush;
379
380     return ( $user->{borrowernumber}, $session->id );
381 }