Bug 16330: Add routes to add, update and delete patrons
[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 use Test::Warn;
23
24 use t::lib::TestBuilder;
25 use t::lib::Mocks;
26
27 use C4::Auth;
28 use Koha::Cities;
29 use Koha::Database;
30
31 my $schema  = Koha::Database->new->schema;
32 my $builder = t::lib::TestBuilder->new;
33
34 # FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
35 # this affects the other REST api tests
36 t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
37
38 my $remote_address = '127.0.0.1';
39 my $t              = Test::Mojo->new('Koha::REST::V1');
40
41 subtest 'list() tests' => sub {
42     plan tests => 2;
43
44     $schema->storage->txn_begin;
45
46     unauthorized_access_tests('GET', undef, undef);
47
48     subtest 'librarian access tests' => sub {
49         plan tests => 8;
50
51         my ($borrowernumber, $sessionid) = create_user_and_session({
52             authorized => 1 });
53         my $patron = Koha::Patrons->find($borrowernumber);
54         Koha::Patrons->search({
55             borrowernumber => { '!=' => $borrowernumber},
56             cardnumber => { LIKE => $patron->cardnumber . "%" }
57         })->delete;
58         Koha::Patrons->search({
59             borrowernumber => { '!=' => $borrowernumber},
60             address2 => { LIKE => $patron->address2 . "%" }
61         })->delete;
62
63         my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
64         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
65         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
66         $t->request_ok($tx)
67           ->status_is(200);
68
69         $tx = $t->ua->build_tx(GET => '/api/v1/patrons?cardnumber='.
70                                   $patron->cardnumber);
71         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
72         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
73         $t->request_ok($tx)
74           ->status_is(200)
75           ->json_is('/0/cardnumber' => $patron->cardnumber);
76
77         $tx = $t->ua->build_tx(GET => '/api/v1/patrons?address2='.
78                                   $patron->address2);
79         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
80         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
81         $t->request_ok($tx)
82           ->status_is(200)
83           ->json_is('/0/address2' => $patron->address2);
84     };
85
86     $schema->storage->txn_rollback;
87 };
88
89 subtest 'get() tests' => sub {
90     plan tests => 3;
91
92     $schema->storage->txn_begin;
93
94     unauthorized_access_tests('GET', -1, undef);
95
96     subtest 'access own object tests' => sub {
97         plan tests => 4;
98
99         my ($patronid, $patronsessionid) = create_user_and_session({
100             authorized => 0 });
101
102         # Access patron's own data even though they have no borrowers flag
103         my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$patronid");
104         $tx->req->cookies({name => 'CGISESSID', value => $patronsessionid});
105         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
106         $t->request_ok($tx)
107           ->status_is(200);
108
109         my $guarantee = $builder->build({
110             source => 'Borrower',
111             value  => {
112                 guarantorid => $patronid,
113             }
114         });
115
116         # Access guarantee's data even though guarantor has no borrowers flag
117         my $guaranteenumber = $guarantee->{borrowernumber};
118         $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$guaranteenumber");
119         $tx->req->cookies({name => 'CGISESSID', value => $patronsessionid});
120         $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
121         $t->request_ok($tx)
122           ->status_is(200);
123     };
124
125     subtest 'librarian access tests' => sub {
126         plan tests => 5;
127
128         my ($patron_id) = create_user_and_session({
129             authorized => 0 });
130         my $patron = Koha::Patrons->find($patron_id);
131         my ($borrowernumber, $sessionid) = create_user_and_session({
132             authorized => 1 });
133         my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/$patron_id");
134         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
135         $t->request_ok($tx)
136           ->status_is(200)
137           ->json_is('/borrowernumber' => $patron_id)
138           ->json_is('/surname' => $patron->surname)
139           ->json_is('/lost' => Mojo::JSON->false );
140     };
141
142     $schema->storage->txn_rollback;
143 };
144
145 subtest 'add() tests' => sub {
146     plan tests => 2;
147
148     $schema->storage->txn_begin;
149
150     my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
151     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
152     my $newpatron = {
153         address      => 'Street',
154         branchcode   => $branchcode,
155         cardnumber   => $branchcode.$categorycode,
156         categorycode => $categorycode,
157         city         => 'Joenzoo',
158         surname      => "TestUser",
159         userid       => $branchcode.$categorycode,
160     };
161
162     unauthorized_access_tests('POST', undef, $newpatron);
163
164     subtest 'librarian access tests' => sub {
165         plan tests => 18;
166
167         my ($borrowernumber, $sessionid) = create_user_and_session({
168             authorized => 1 });
169
170         $newpatron->{branchcode} = "nonexistent"; # Test invalid branchcode
171         my $tx = $t->ua->build_tx(POST => "/api/v1/patrons" =>json => $newpatron);
172         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
173         $t->request_ok($tx)
174           ->status_is(400)
175           ->json_is('/error' => "Given branchcode does not exist");
176         $newpatron->{branchcode} = $branchcode;
177
178         $newpatron->{categorycode} = "nonexistent"; # Test invalid patron category
179         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
180         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
181         $t->request_ok($tx)
182           ->status_is(400)
183           ->json_is('/error' => "Given categorycode does not exist");
184         $newpatron->{categorycode} = $categorycode;
185
186         $newpatron->{falseproperty} = "Non existent property";
187         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
188         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
189         $t->request_ok($tx)
190           ->status_is(400);
191         delete $newpatron->{falseproperty};
192
193         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
194         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
195         $t->request_ok($tx)
196           ->status_is(201, 'Patron created successfully')
197           ->json_has('/borrowernumber', 'got a borrowernumber')
198           ->json_is('/cardnumber', $newpatron->{ cardnumber })
199           ->json_is('/surname' => $newpatron->{ surname })
200           ->json_is('/firstname' => $newpatron->{ firstname });
201         $newpatron->{borrowernumber} = $tx->res->json->{borrowernumber};
202
203         $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
204         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
205         $t->request_ok($tx)
206           ->status_is(409)
207           ->json_has('/error', 'Fails when trying to POST duplicate'.
208                      ' cardnumber or userid')
209           ->json_has('/conflict', {
210                         userid => $newpatron->{ userid },
211                         cardnumber => $newpatron->{ cardnumber }
212                     }
213             );
214     };
215
216     $schema->storage->txn_rollback;
217 };
218
219 subtest 'edit() tests' => sub {
220     plan tests => 3;
221
222     $schema->storage->txn_begin;
223
224     unauthorized_access_tests('PUT', 123, {email => 'nobody@example.com'});
225
226     subtest 'patron modifying own data' => sub {
227         plan tests => 7;
228
229         my ($borrowernumber, $sessionid) = create_user_and_session({
230             authorized => 0 });
231         my $patron = Koha::Patrons->find($borrowernumber)->TO_JSON;
232
233         t::lib::Mocks::mock_preference("OPACPatronDetails", 0);
234         my $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
235                             $patron->{borrowernumber} => json => $patron);
236         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
237         $t->request_ok($tx)
238           ->status_is(403, 'OPACPatronDetails off - modifications not allowed.');
239
240         t::lib::Mocks::mock_preference("OPACPatronDetails", 1);
241         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
242                             $patron->{borrowernumber} => json => $patron);
243         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
244         $t->request_ok($tx)
245           ->status_is(204, 'Updating myself with my current data');
246
247         $patron->{'firstname'} = "noob";
248         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
249                             $patron->{borrowernumber} => json => $patron);
250         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
251         $t->request_ok($tx)
252           ->status_is(202, 'Updating myself with my current data');
253
254         # Approve changes
255         Koha::Patron::Modifications->find({
256             borrowernumber => $patron->{borrowernumber},
257             firstname => "noob"
258         })->approve;
259         is(Koha::Patrons->find({
260             borrowernumber => $patron->{borrowernumber}})->firstname,
261            "noob", "Changes approved");
262     };
263
264     subtest 'librarian access tests' => sub {
265         plan tests => 20;
266
267         t::lib::Mocks::mock_preference('minPasswordLength', 1);
268         my ($borrowernumber, $sessionid) = create_user_and_session({
269             authorized => 1 });
270         my ($borrowernumber2, undef) = create_user_and_session({
271             authorized => 0 });
272         my $patron    = Koha::Patrons->find($borrowernumber2);
273         my $newpatron = Koha::Patrons->find($borrowernumber2)->TO_JSON;
274
275         my $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/-1" =>
276                                   json => $newpatron);
277         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
278         $t->request_ok($tx)
279           ->status_is(404)
280           ->json_has('/error', 'Fails when trying to PUT nonexistent patron');
281
282         $newpatron->{categorycode} = 'nonexistent';
283         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
284                     $newpatron->{borrowernumber} => json => $newpatron
285         );
286         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
287         $t->request_ok($tx)
288           ->status_is(400)
289           ->json_is('/error' => "Given categorycode does not exist");
290         $newpatron->{categorycode} = $patron->categorycode;
291
292         $newpatron->{branchcode} = 'nonexistent';
293         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
294                     $newpatron->{borrowernumber} => json => $newpatron
295         );
296         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
297         $t->request_ok($tx)
298           ->status_is(400)
299           ->json_is('/error' => "Given branchcode does not exist");
300         $newpatron->{branchcode} = $patron->branchcode;
301
302         $newpatron->{falseproperty} = "Non existent property";
303         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
304                     $newpatron->{borrowernumber} => json => $newpatron);
305         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
306         $t->request_ok($tx)
307           ->status_is(400)
308           ->json_is('/errors/0/message' =>
309                     'Properties not allowed: falseproperty.');
310         delete $newpatron->{falseproperty};
311
312         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
313                     $borrowernumber => json => $newpatron);
314         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
315         $t->request_ok($tx)
316           ->status_is(409)
317           ->json_has('/error' => "Fails when trying to update to an existing"
318                      ."cardnumber or userid")
319           ->json_has('/conflict', {
320                 cardnumber => $newpatron->{ cardnumber },
321                 userid => $newpatron->{ userid }
322                 }
323             );
324
325         $newpatron->{ cardnumber } = $borrowernumber.$borrowernumber2;
326         $newpatron->{ userid } = "user".$borrowernumber.$borrowernumber2;
327         $newpatron->{ surname } = "user".$borrowernumber.$borrowernumber2;
328
329         $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" .
330                     $newpatron->{borrowernumber} => json => $newpatron);
331         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
332         $t->request_ok($tx)
333           ->status_is(200, 'Patron updated successfully')
334           ->json_has($newpatron);
335         is(Koha::Patrons->find($newpatron->{borrowernumber})->cardnumber,
336            $newpatron->{ cardnumber }, 'Patron is really updated!');
337     };
338
339     $schema->storage->txn_rollback;
340 };
341
342 subtest 'delete() tests' => sub {
343     plan tests => 2;
344
345     $schema->storage->txn_begin;
346
347     unauthorized_access_tests('DELETE', 123, undef);
348
349     subtest 'librarian access test' => sub {
350         plan tests => 4;
351
352         my ($borrowernumber, $sessionid) = create_user_and_session({
353             authorized => 1 });
354         my ($borrowernumber2, $sessionid2) = create_user_and_session({
355             authorized => 0 });
356
357         my $tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/-1");
358         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
359         $t->request_ok($tx)
360           ->status_is(404, 'Patron not found');
361
362         $tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/$borrowernumber2");
363         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
364         $t->request_ok($tx)
365           ->status_is(200, 'Patron deleted successfully');
366     };
367
368     $schema->storage->txn_rollback;
369 };
370
371 # Centralized tests for 401s and 403s assuming the endpoint requires
372 # borrowers flag for access
373 sub unauthorized_access_tests {
374     my ($verb, $patronid, $json) = @_;
375
376     my $endpoint = '/api/v1/patrons';
377     $endpoint .= ($patronid) ? "/$patronid" : '';
378
379     subtest 'unauthorized access tests' => sub {
380         plan tests => 5;
381
382         my $tx = $t->ua->build_tx($verb => $endpoint => json => $json);
383         $t->request_ok($tx)
384           ->status_is(401);
385
386         my ($borrowernumber, $sessionid) = create_user_and_session({
387             authorized => 0 });
388
389         $tx = $t->ua->build_tx($verb => $endpoint => json => $json);
390         $tx->req->cookies({name => 'CGISESSID', value => $sessionid});
391         $t->request_ok($tx)
392           ->status_is(403)
393           ->json_is('/required_permissions', {"borrowers" => "1"});
394     };
395 }
396
397 sub create_user_and_session {
398
399     my $args  = shift;
400     my $flags = ( $args->{authorized} ) ? 16 : 0;
401     my $dbh   = C4::Context->dbh;
402
403     my $user = $builder->build(
404         {
405             source => 'Borrower',
406             value  => {
407                 flags => $flags,
408                 gonenoaddress => 0,
409                 lost => 0,
410                 email => 'nobody@example.com',
411                 emailpro => 'nobody@example.com',
412                 B_email => 'nobody@example.com',
413             }
414         }
415     );
416
417     # Create a session for the authorized user
418     my $session = C4::Auth::get_session('');
419     $session->param( 'number',   $user->{borrowernumber} );
420     $session->param( 'id',       $user->{userid} );
421     $session->param( 'ip',       '127.0.0.1' );
422     $session->param( 'lasttime', time() );
423     $session->flush;
424
425     return ( $user->{borrowernumber}, $session->id );
426 }